Skip to content

Commit 6d11e41

Browse files
committed
Merge branch 'main' into 13-add-ui
2 parents 567ef97 + 285400c commit 6d11e41

16 files changed

+109
-39
lines changed

.github/workflows/python-package.yml

+10-6
Original file line numberDiff line numberDiff line change
@@ -13,33 +13,37 @@ jobs:
1313
strategy:
1414
fail-fast: false
1515
matrix:
16-
python-version: ["3.8", "3.9", "3.10"]
16+
python-version: ["3.8", "3.9", "3.10", "3.11"]
1717

1818
steps:
1919
- uses: actions/checkout@v2
2020
- name: Set up Python ${{ matrix.python-version }}
21-
uses: actions/setup-python@v2
21+
uses: actions/setup-python@v4
2222
with:
2323
python-version: ${{ matrix.python-version }}
24+
cache: 'pip' # caching pip dependencies
25+
cache-dependency-path: |
26+
**/setup.cfg
27+
**/requirements*.txt
2428
- name: Install dependencies
2529
run: |
2630
python -m pip install --upgrade pip
2731
if [ -f requirements-test.txt ]; then pip install -r requirements-test.txt; fi
2832
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
33+
- name: Sort imports with isort
34+
run: isort . --check --color --diff
35+
- name: Code format with black
36+
run: black . --check --color --diff
2937
- name: Lint with flake8
3038
run: |
3139
# stop the build if there are Python syntax errors or undefined names
3240
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
3341
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
3442
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
35-
- name: Code format with black
36-
run: |
37-
black . --check --color --diff
3843
- name: Test with pytest
3944
run: |
4045
pytest --cov=ffbot --cov-report=xml
4146
- name: "Upload coverage to Codecov"
4247
uses: codecov/codecov-action@v2
4348
with:
4449
fail_ci_if_error: true
45-

.github/workflows/pythonpublish.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
- name: Set up Python
1313
uses: actions/setup-python@v1
1414
with:
15-
python-version: '3.7'
15+
python-version: '3.11'
1616
- name: Install dependencies
1717
run: |
1818
python -m pip install --upgrade pip

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,4 @@ chromedriver.exe
109109
.idea
110110
credentials.json
111111
*.mps
112+
.vscode

.pre-commit-config.yaml

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
repos:
2+
- repo: https://github.com/pre-commit/pre-commit-hooks
3+
rev: v4.5.0
4+
hooks:
5+
- id: trailing-whitespace
6+
exclude_types: [python]
7+
- id: end-of-file-fixer
8+
- id: check-yaml
9+
- id: check-added-large-files
10+
- repo: https://github.com/PyCQA/isort
11+
rev: 5.13.2
12+
hooks:
13+
- id: isort
14+
- repo: https://github.com/psf/black
15+
rev: 24.3.0
16+
hooks:
17+
- id: black

README.md

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# fantasy-football-bot (ffbot)
22

33
[![PyPI Latest Release](https://img.shields.io/pypi/v/ffbot.svg)](https://pypi.org/project/ffbot/)
4-
[![PyPI downloads](https://pepy.tech/badge/ffbot)](https://pepy.tech/project/ffbot)
4+
[![PyPI downloads](https://static.pepy.tech/badge/ffbot)](https://pepy.tech/project/ffbot)
55
[![License](https://img.shields.io/github/license/amarvin/fantasy-football-bot)](https://github.com/amarvin/fantasy-football-bot/blob/main/LICENSE)
66
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
7+
[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/)
78
[![codecov](https://codecov.io/gh/amarvin/fantasy-football-bot/branch/main/graph/badge.svg?token=CH6M9DR7VX)](https://codecov.io/gh/amarvin/fantasy-football-bot)
89

910
Automate playing Yahoo Fantasy Football
@@ -23,14 +24,16 @@ pip install ffbot
2324
### Scrape player forecasts
2425

2526
To connect to your Yahoo league and team, you need your league ID and team ID.
26-
Visit your team at https://football.fantasysports.yahoo.com/f1/, and the url will also include your league and team ID.
27+
Visit your team at <https://football.fantasysports.yahoo.com/f1/>, and the url will also include your league and team ID.
2728

2829
```python
2930
>>> LEAGUE = 123456
3031
>>> TEAM = 1
3132
>>> POSITIONS = "QB, WR, WR, WR, RB, RB, TE, W/R/T, K, DEF, BN, BN, BN, BN, IR"
3233
>>> week = ffbot.current_week()
3334
>>> df = ffbot.scrape(LEAGUE)
35+
>>> # If playing an Individual Defensive Player (IDP) league, then scrape additional players with:
36+
>>> # df = ffbot.scrape(LEAGUE, is_IDP=True)
3437
Scraping all QB...
3538
Scraping all WR...
3639
Scraping all RB...
@@ -73,10 +76,11 @@ Only one Waiver claim (for Jordon Howard) increases discounted points.
7376

7477
## Contribution
7578

76-
Please add Issues or submit Pull requests!
79+
Please add Issues or submit Pull Requests!
7780

78-
For local development, install optional testing dependencies using
81+
For local development, install optional testing dependencies and pre-commit hooks using
7982

8083
```sh
8184
pip install ffbot[test]
85+
pre-commit install
8286
```

demo.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import ffbot
22

3-
43
# Yahoo league/team id
54
# Visit your team at https://football.fantasysports.yahoo.com/f1/, and the url will also include your league and team ID
65
LEAGUE = 123456
@@ -10,6 +9,8 @@
109

1110
# Scrape data for current and available players, and their point forecasts for each week
1211
df = ffbot.scrape(LEAGUE)
12+
# If playing an Individual Defensive Player (IDP) league, then scrape additional players with:
13+
# df = ffbot.scrape(LEAGUE, is_IDP=True)
1314

1415
# Optional save data to CSV, and load latest data
1516
# ffbot.save(df, week)

ffbot/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from .constants import VERSION
22
from .optimizer import optimize # noqa: F401,E402
33
from .scraper import current_week, scrape # noqa: F401,E402
4-
from .utils import save, load # noqa: F401,E402
4+
from .utils import load, save # noqa: F401,E402
55

66
__version__ = VERSION

ffbot/constants.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = "1.1.0"
1+
VERSION = "1.2.4"

ffbot/optimizer.py

+29-9
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
from collections import Counter
22

33
import pandas as pd
4+
from loguru import logger
45
from pulp import (
6+
PULP_CBC_CMD,
57
LpBinary,
68
LpContinuous,
79
LpMaximize,
810
LpProblem,
911
LpStatus,
10-
lpSum,
1112
LpVariable,
12-
PULP_CBC_CMD,
13+
lpSum,
1314
value,
1415
)
1516

16-
1717
IR_STATUSES = {
1818
"COVID", # e.g. COVID-19
1919
"IR", # e.g. IR, IR-R
@@ -33,19 +33,36 @@ def optimize(df, week, team, positions):
3333
# Game rules
3434
positions = [x.strip() for x in positions.split(",")]
3535
PossiblePositions = dict(
36-
QB={"QB"},
37-
WR={"WR", "W/R/T"},
38-
RB={"RB", "W/R/T"},
39-
TE={"TE", "W/R/T"},
36+
QB={"QB", "Q/W/R/T"},
37+
WR={"WR", "W/R/T", "W/T", "W/R", "Q/W/R/T"},
38+
RB={"RB", "W/R/T", "W/R", "Q/W/R/T"},
39+
TE={"TE", "W/R/T", "W/T", "Q/W/R/T"},
4040
K={"K"},
41-
DEF={"DEF"},
41+
DEF={"DEF", "D/ST"},
42+
# IDP positions
43+
CB={"CB", "D", "DB"},
44+
DE={"D", "DE", "DL"},
45+
DT={"D", "DL", "DT"},
46+
LB={"D", "LB"},
47+
S={"D", "DB", "S"},
4248
)
49+
# Remove positions not available in this league
50+
for key, values in PossiblePositions.items():
51+
PossiblePositions[key] = set(
52+
position for position in values if position in positions
53+
)
54+
PossiblePositions = {
55+
key: values for key, values in PossiblePositions.items() if values
56+
}
57+
# Players that can play multiple positions
4358
for position in df["Position"].unique():
4459
if position not in PossiblePositions:
4560
# There is a player that can play multiple positions, so consider those options too
4661
PossiblePositions[position] = set()
4762
for n in position.split(","):
48-
PossiblePositions[position].update(PossiblePositions[n.strip()])
63+
n = n.strip()
64+
if n in PossiblePositions:
65+
PossiblePositions[position].update(PossiblePositions[n])
4966
PositionMax = Counter(positions)
5067
POSITIONS = PositionMax.keys()
5168

@@ -100,6 +117,7 @@ def optimize(df, week, team, positions):
100117
for n in POSITIONS
101118
if (p, n) in PlayerPosition
102119
]
120+
logger.info("Optimizer pre-processed data")
103121

104122
# Define optimization problem
105123
prob = LpProblem("football", LpMaximize)
@@ -153,6 +171,7 @@ def optimize(df, week, team, positions):
153171
lpSum(assign[p, t, n] for p in PLAYERS if (p, n) in PlayerPosition)
154172
<= PositionMax[n]
155173
)
174+
logger.info("Optimizer starting...")
156175

157176
# Solve optimization problem
158177
solutions_headers = ["Add", "Drop", "Total points", "Discounted points", "VOR"]
@@ -317,4 +336,5 @@ def optimize(df, week, team, positions):
317336
df_opt = pd.DataFrame(solutions, columns=solutions_headers)
318337
df_opt = df_opt.round(2)
319338
df_opt.fillna("", inplace=True)
339+
logger.info("Optimizer finished")
320340
return df_opt

ffbot/scraper.py

+27-10
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
11
from datetime import datetime
2+
from io import StringIO
23

3-
from bs4 import BeautifulSoup as bs
4-
from loguru import logger
54
import numpy as np
65
import pandas as pd
76
import requests
7+
from bs4 import BeautifulSoup as bs
8+
from loguru import logger
89
from requests.adapters import HTTPAdapter
910
from tqdm import tqdm
1011
from urllib3.util import Retry
1112
from user_agent import generate_user_agent
1213

13-
1414
# A public league for current week and player IDs
15-
PUBLIC_LEAGUE = 39452
15+
PUBLIC_LEAGUE = 16
16+
PUBLIC_LEAGUE_IDP = 762
17+
SEARCH_PLAYER_GROUPS = ["QB", "WR", "RB", "TE", "K", "DEF"]
18+
SEARCH_PLAYER_GROUPS_IDP = ["QB", "WR", "RB", "TE", "K", "D", "DB", "DL", "LB"]
1619

1720

1821
def create_session():
1922
"""Create requests session with retries and random user-agent"""
2023
s = requests.Session()
2124
s.headers = {
25+
"Accept": "text/html",
26+
"Accept-Encoding": "gzip, deflate, br",
27+
"Accept-Language": "en-US",
2228
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
2329
"User-Agent": generate_user_agent(),
2430
}
@@ -30,18 +36,19 @@ def create_session():
3036
return s
3137

3238

33-
def scrape(league):
39+
def scrape(league, is_IDP: bool = False):
3440
"""Scrape data
3541
3642
:param league: league ID
43+
:param is_IDP: (bool) is this a individual defense player (IDP) league?
3744
"""
3845

3946
# Start timer
4047
startTime = datetime.now()
4148

4249
# Scrape player IDs and teams from a public league
4350
data = set()
44-
groups = ["QB", "WR", "RB", "TE", "K", "DEF"]
51+
groups = SEARCH_PLAYER_GROUPS_IDP if is_IDP else SEARCH_PLAYER_GROUPS
4552
s = create_session()
4653
for group in groups:
4754
logger.info("Scraping all {}...".format(group))
@@ -51,7 +58,7 @@ def scrape(league):
5158
s.headers["User-Agent"] = generate_user_agent()
5259
r = s.get(
5360
"https://football.fantasysports.yahoo.com/f1/{}/players".format(
54-
PUBLIC_LEAGUE
61+
PUBLIC_LEAGUE_IDP if is_IDP else PUBLIC_LEAGUE
5562
),
5663
params=dict(
5764
count=i * 25,
@@ -64,12 +71,14 @@ def scrape(league):
6471
i += 1
6572
soup = bs(r.text, "lxml")
6673
table = soup.select_one("#players-table table")
74+
if not table:
75+
break
6776
rows = table.select("tbody tr")
6877
if not rows:
6978
break
7079
for row in rows:
7180
td = row.select("td")[1]
72-
ID = td.select_one("span.player-status a")["data-ys-playerid"]
81+
ID = td.select_one(".player-status a")["data-ys-playerid"]
7382
ID = int(ID)
7483
team = td.select_one(".ysf-player-name span").text
7584
team = team.split()[0]
@@ -115,7 +124,7 @@ def get_projections(row):
115124
row["% Owned"] = playerinfo.select_one("dd.owned").text.split()[0]
116125

117126
# Weekly projections
118-
df2 = pd.read_html(html)[0]
127+
df2 = pd.read_html(StringIO(html))[0]
119128
for _, row2 in df2.iterrows():
120129
week = "Week {}".format(row2["Week"])
121130
points = row2["Fan Pts"]
@@ -149,7 +158,15 @@ def get_projections(row):
149158
columns = ["Week {}".format(i) for i in range(current_week(), 18)]
150159
df["Remaining"] = df[columns].sum(axis=1)
151160
available = df.loc[df["Owner ID"].isnull()]
152-
means = available.groupby(["Position"])["Remaining"].nlargest(3).mean(level=0)
161+
means = (
162+
available.groupby(["Position"])["Remaining"].nlargest(3).groupby(level=0).mean()
163+
)
164+
for positions in means.index:
165+
if "," in positions:
166+
for position in positions.split(","):
167+
position = position.strip()
168+
if position not in means:
169+
means[position] = means[positions]
153170
df["VOR"] = df.apply(
154171
lambda row: row["Remaining"]
155172
- max(means[n.strip()] for n in row["Position"].split(",")),

ffbot/utils.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import re
12
from datetime import datetime
23
from os import listdir, makedirs
34
from os.path import exists, getctime, isfile, join, split
4-
import re
55

66
import pandas as pd
77

pyproject.toml

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[tool.isort]
2+
profile = "black"

requirements-test.txt

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
black
22
flake8
3+
isort[colors]
4+
pre-commit
35
pytest
46
pytest-cov

setup.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from io import open
22
from os import path
3-
from setuptools import setup, find_packages
43

5-
from ffbot.constants import VERSION
4+
from setuptools import find_packages, setup
65

6+
from ffbot.constants import VERSION
77

88
here = path.abspath(path.dirname(__file__))
99

@@ -32,7 +32,7 @@ def get_requirements(kind: str = None):
3232
author="Alex Marvin",
3333
author_email="alex.marvin@gmail.com",
3434
classifiers=[
35-
"Development Status :: 3 - Alpha",
35+
"Development Status :: 5 - Production/Stable",
3636
"License :: OSI Approved :: MIT License",
3737
"Programming Language :: Python :: 3",
3838
],

tests/test_optimizer.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import pandas as pd
22

3-
from . import POSITIONS, SCRAPER_FILE, TEAM
43
import ffbot
54

5+
from . import POSITIONS, SCRAPER_FILE, TEAM
6+
67

78
def test_optimize():
89
df, week = ffbot.load(SCRAPER_FILE)

0 commit comments

Comments
 (0)