16 |
17 | Note that scores have not yet been finalized for this
18 | week and the {dataType} {dataType.toLowerCase().includes('list') ? 'are' : 'is'} likely to change.
19 |
20 | Please check back on Tuesday morning for the final
21 | results.
22 |
23 |
24 | );
25 | };
26 |
27 | PendingDataNotice.propTypes = {
28 | dataType: PropTypes.string.isRequired,
29 | isPending: PropTypes.bool.isRequired
30 | };
31 |
32 | export default PendingDataNotice;
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Desi Pilla
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/backend/fantasy_stats/migrations/0002_leagueinfo.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.7 on 2021-09-18 19:06
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("fantasy_stats", "0001_initial"),
9 | ]
10 |
11 | operations = [
12 | migrations.CreateModel(
13 | name="LeagueInfo",
14 | fields=[
15 | (
16 | "id",
17 | models.BigAutoField(
18 | auto_created=True,
19 | primary_key=True,
20 | serialize=False,
21 | verbose_name="ID",
22 | ),
23 | ),
24 | ("league_id", models.IntegerField()),
25 | ("league_year", models.IntegerField()),
26 | ("swid", models.CharField(max_length=50)),
27 | ("espn_s2", models.CharField(max_length=300)),
28 | (
29 | "league_name",
30 | models.CharField(default="", max_length=50),
31 | ),
32 | ],
33 | ),
34 | ]
35 |
--------------------------------------------------------------------------------
/frontend/src/components/SimulationSelector.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const SimulationSelector = ({ nSimulations, setNSimulations }) => {
4 | const handleChange = (event) => {
5 | const value = event.target.value;
6 | setNSimulations(value === "--" ? null : parseInt(value, 10)); // Set to null if "--" is selected
7 | };
8 |
9 | return (
10 |
11 | Playoff odds are calculated by generating {nSimulations || "--"} Monte Carlo simulations and comparing the results.
12 |
13 | You can choose a different n by selecting one of these options:
14 |
26 |
27 | You can also edit the n_simulations= value in the URL. However, the more simulations that are run, the longer the page will take to load.
28 |
Right click anywhere on the screen (Chrome browser only) and click Inspect.
10 |
In the window that appears on the right, click Application on the top bar (you may have to click the dropdown arrow next to Elements, Console, Sources...).
11 |
On the left, navigate to Storage > Cookies > http://fantasy.espn.com.
12 |
Scroll down in the table to the right until you find SWID. Copy & paste the alphanumeric string in the Value column (without the curly brackets). It should look something like: 43B70875-0C4B-428L-B608-759A4BB28FA1
13 |
Next, keep scrolling until you find espn_s2. Again, copy and paste the alphanumeric string in the Value column. This code will be much longer and won't have curly brackets in it.
14 |
15 |
16 | );
17 | };
18 |
19 | export default CookiesInstructionsBox;
20 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "doritostats"
3 | version = "4.0.0"
4 | description = "This project aims to make ESPN Fantasy Football statistics easily available. With the introduction of version 3 of the ESPN's API, this structure creates leagues, teams, and player classes that allow for advanced data analytics and the potential for many new features to be added."
5 | authors = ["Desi Pilla "]
6 | license = "https://github.com/DesiPilla/espn-api-v3/blob/master/LICENSE"
7 | readme = "README.md"
8 | packages = [{include = "backend"}, { from = "backend/src", include = "doritostats" }]
9 |
10 | [tool.poetry.dependencies]
11 | python = ">=3.12,<3.13"
12 | espn-api = "0.44.1"
13 | numpy = "^2.1.2"
14 | pandas = "^2.2.2"
15 | pytest = "^8.3.3"
16 | python-dotenv = "^0.21.0"
17 | requests = "^2.32.3"
18 | scikit-learn = "^1.5.0"
19 |
20 | black = "24.3.0"
21 | certifi = "^2024.8.30"
22 | dj-database-url = "^2.2.0"
23 | gunicorn = "^23.0.0"
24 | joblib = "^1.4.2"
25 | scipy = "^1.14.1"
26 | setuptools = "^75.2.0"
27 | sqlalchemy = "^2.0.36"
28 | urllib3 = "^2.2.3"
29 | whitenoise = { extras = ["brotli"], version = "^6.7.0" }
30 | Django = "^5.1.2"
31 | psycopg2-binary = "^2.9.10"
32 | pytz = "^2024.2"
33 | djangorestframework = "^3.15.2"
34 | django-cors-headers = "^4.7.0"
35 | resend = "^2.13.1"
36 |
37 | [build-system]
38 | requires = ["poetry-core"]
39 | build-backend = "poetry.core.masonry.api"
40 |
--------------------------------------------------------------------------------
/frontend/src/components/AwardsReact.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {allAwardsData} from './Awards';
3 |
4 |
5 |
6 | function MakeRow(awardRow){
7 | return (
8 |
16 | We're sorry, but league homepages are not accessible until Week 1 has
17 | begun. Please check back later for the information you're looking for.
18 |
23 | We're sorry, but playoff simulations are not accessible until
24 | Week 4 has completed. Please check back later for the
25 | information you're looking for.
26 |
27 | );
28 | } else {
29 | return
This page is not available yet. Please check back later.
63 |
64 | );
65 | };
66 |
67 | SeasonTeamRecordsTable.propTypes = {
68 | bestTeamStats: PropTypes.arrayOf(
69 | PropTypes.shape({
70 | label: PropTypes.string.isRequired,
71 | owner: PropTypes.string.isRequired,
72 | value: PropTypes.string.isRequired,
73 | })
74 | ),
75 | worstTeamStats: PropTypes.arrayOf(
76 | PropTypes.shape({
77 | label: PropTypes.string.isRequired,
78 | owner: PropTypes.string.isRequired,
79 | value: PropTypes.string.isRequired,
80 | })
81 | ),
82 | };
83 |
84 | export default SeasonTeamRecordsTable;
85 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
13 |
14 | The page will reload when you make changes.\
15 | You may also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!**
35 |
36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
39 |
40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `npm run build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/backend/src/doritostats/pick_value.csv:
--------------------------------------------------------------------------------
1 | pick,value,perc_of_first
2 | 1,90.3,100.00%
3 | 2,78.3,86.71%
4 | 3,71.3,78.96%
5 | 4,66.4,73.53%
6 | 5,62.5,69.21%
7 | 6,59.4,65.78%
8 | 7,56.7,62.79%
9 | 8,54.4,60.24%
10 | 9,52.3,57.92%
11 | 10,50.5,55.92%
12 | 11,48.9,54.15%
13 | 12,47.4,52.49%
14 | 13,46,50.94%
15 | 14,44.7,49.50%
16 | 15,43.5,48.17%
17 | 16,42.4,46.95%
18 | 17,41.4,45.85%
19 | 18,40.4,44.74%
20 | 19,39.4,43.63%
21 | 20,38.5,42.64%
22 | 21,37.7,41.75%
23 | 22,36.9,40.86%
24 | 23,36.1,39.98%
25 | 24,35.4,39.20%
26 | 25,34.7,38.43%
27 | 26,34,37.65%
28 | 27,33.4,36.99%
29 | 28,32.7,36.21%
30 | 29,32.1,35.55%
31 | 30,31.5,34.88%
32 | 31,31,34.33%
33 | 32,30.4,33.67%
34 | 33,29.9,33.11%
35 | 34,29.4,32.56%
36 | 35,28.9,32.00%
37 | 36,28.4,31.45%
38 | 37,27.9,30.90%
39 | 38,27.4,30.34%
40 | 39,27,29.90%
41 | 40,26.6,29.46%
42 | 41,26.1,28.90%
43 | 42,25.7,28.46%
44 | 43,25.3,28.02%
45 | 44,24.9,27.57%
46 | 45,24.5,27.13%
47 | 46,24.1,26.69%
48 | 47,23.8,26.36%
49 | 48,23.4,25.91%
50 | 49,23.1,25.58%
51 | 50,22.7,25.14%
52 | 51,22.4,24.81%
53 | 52,22,24.36%
54 | 53,21.7,24.03%
55 | 54,21.4,23.70%
56 | 55,21.1,23.37%
57 | 56,20.7,22.92%
58 | 57,20.4,22.59%
59 | 58,20.1,22.26%
60 | 59,19.8,21.93%
61 | 60,19.6,21.71%
62 | 61,19.3,21.37%
63 | 62,19,21.04%
64 | 63,18.7,20.71%
65 | 64,18.4,20.38%
66 | 65,18.2,20.16%
67 | 66,17.9,19.82%
68 | 67,17.6,19.49%
69 | 68,17.4,19.27%
70 | 69,17.1,18.94%
71 | 70,16.9,18.72%
72 | 71,16.6,18.38%
73 | 72,16.4,18.16%
74 | 73,16.2,17.94%
75 | 74,15.9,17.61%
76 | 75,15.7,17.39%
77 | 76,15.5,17.17%
78 | 77,15.2,16.83%
79 | 78,15,16.61%
80 | 79,14.8,16.39%
81 | 80,14.6,16.17%
82 | 81,14.4,15.95%
83 | 82,14.2,15.73%
84 | 83,13.9,15.39%
85 | 84,13.7,15.17%
86 | 85,13.5,14.95%
87 | 86,13.3,14.73%
88 | 87,13.1,14.51%
89 | 88,12.9,14.29%
90 | 89,12.7,14.06%
91 | 90,12.5,13.84%
92 | 91,12.4,13.73%
93 | 92,12.2,13.51%
94 | 93,12,13.29%
95 | 94,11.8,13.07%
96 | 95,11.6,12.85%
97 | 96,11.4,12.62%
98 | 97,11.2,12.40%
99 | 98,11.1,12.29%
100 | 99,10.9,12.07%
101 | 100,10.7,11.85%
102 | 101,10.6,11.74%
103 | 102,10.4,11.52%
104 | 103,10.2,11.30%
105 | 104,10,11.07%
106 | 105,9.9,10.96%
107 | 106,9.7,10.74%
108 | 107,9.6,10.63%
109 | 108,9.4,10.41%
110 | 109,9.2,10.19%
111 | 110,9.1,10.08%
112 | 111,8.9,9.86%
113 | 112,8.8,9.75%
114 | 113,8.6,9.52%
115 | 114,8.5,9.41%
116 | 115,8.3,9.19%
117 | 116,8.2,9.08%
118 | 117,8,8.86%
119 | 118,7.9,8.75%
120 | 119,7.7,8.53%
121 | 120,7.6,8.42%
122 | 121,7.4,8.19%
123 | 122,7.3,8.08%
124 | 123,7.1,7.86%
125 | 124,7,7.75%
126 | 125,6.9,7.64%
127 | 126,6.7,7.42%
128 | 127,6.6,7.31%
129 | 128,6.5,7.20%
130 | 129,6.3,6.98%
131 | 130,6.2,6.87%
132 | 131,6.1,6.76%
133 | 132,5.9,6.53%
134 | 133,5.8,6.42%
135 | 134,5.7,6.31%
136 | 135,5.5,6.09%
137 | 136,5.4,5.98%
138 | 137,5.3,5.87%
139 | 138,5.2,5.76%
140 | 139,5,5.54%
141 | 140,4.9,5.43%
142 | 141,4.8,5.32%
143 | 142,4.7,5.20%
144 | 143,4.5,4.98%
145 | 144,4.4,4.87%
146 | 145,4.3,4.76%
147 | 146,4.2,4.65%
148 | 147,4.1,4.54%
149 | 148,3.9,4.32%
150 | 149,3.8,4.21%
151 | 150,3.7,4.10%
152 | 151,3.6,3.99%
153 | 152,3.5,3.88%
154 | 153,3.4,3.77%
155 | 154,3.3,3.65%
156 | 155,3.1,3.43%
157 | 156,3,3.32%
158 | 157,2.9,3.21%
159 | 158,2.8,3.10%
160 | 159,2.7,2.99%
161 | 160,2.6,2.88%
162 | 161,2.5,2.77%
163 | 162,2.4,2.66%
164 | 163,2.3,2.55%
165 | 164,2.2,2.44%
166 | 165,2.1,2.33%
167 | 166,2,2.21%
168 | 167,1.9,2.10%
169 | 168,1.8,1.99%
170 | 169,1.7,1.88%
171 | 170,1.6,1.77%
172 | 171,1.4,1.55%
173 | 172,1.3,1.44%
174 | 173,1.2,1.33%
175 | 174,1.1,1.22%
176 | 175,1,1.11%
177 | 176,1,1.11%
178 | 177,0.9,1.00%
179 | 178,0.8,0.89%
180 | 179,0.7,0.78%
181 | 180,0.6,0.66%
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at dmpilla@udel.edu. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/frontend/src/components/ReturningLeagueSelector.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import { getCookie } from "../utils/csrf";
4 | import { fetchWithRetry } from "../utils/api";
5 |
6 | const ReturningLeagueSelector = ({ dropdownClassName }) => {
7 | const [leaguesPreviousDistinct, setLeaguesPreviousDistinct] = useState([]);
8 | const [loading, setLoading] = useState(false);
9 | const [error, setError] = useState("");
10 | const navigate = useNavigate();
11 |
12 | useEffect(() => {
13 | const retries = 3; // Configurable number of retries
14 | fetchWithRetry("/api/distinct-leagues-previous/", {}, retries)
15 | .then((res) => res.json())
16 | .then((data) => {
17 | setLeaguesPreviousDistinct(data);
18 | })
19 | .catch((error) =>
20 | console.error("Error fetching distinct leagues:", error)
21 | );
22 | }, []);
23 |
24 | const handleSelect = async (e) => {
25 | const leagueId = e.target.value;
26 | if (!leagueId) return;
27 |
28 | setLoading(true);
29 | setError("");
30 |
31 | const retries = 2;
32 |
33 | try {
34 | const csrftoken = getCookie("csrftoken");
35 |
36 | const response = await fetchWithRetry(
37 | `/api/copy-old-league/${leagueId}/`,
38 | {
39 | method: "POST",
40 | headers: {
41 | "Content-Type": "application/json",
42 | "X-CSRFToken": csrftoken,
43 | },
44 | credentials: "include",
45 | },
46 | retries
47 | );
48 |
49 | const data = await response.json();
50 |
51 | if (response.ok && data.redirect_url) {
52 | navigate(data.redirect_url);
53 | } else {
54 | setError(data.error || "Failed to copy league.");
55 | }
56 | } catch (err) {
57 | console.error("Error copying league:", err);
58 | setError("An unexpected error occurred.");
59 | } finally {
60 | setLoading(false);
61 | }
62 | };
63 |
64 | return (
65 | <>
66 |
67 | Returning for the new season? Select your old league and easily
68 | add it for the 2025-26 season.
69 |
66 |
67 | Note that for this league, {playoffTeams} teams make the
68 | playoffs.
69 |
70 |
71 |
72 | All playoff odds are based on simulation results alone.
73 | Values of 0% or 100% do not necessarily mean that a team has
74 | been mathematically eliminated or clinched a playoff spot.
75 |
76 |
63 |
64 | Note that for this league, {playoffTeams} teams make the
65 | playoffs.
66 |
67 |
68 |
69 | All percentages are based on simulation results alone.
70 | Values of 0% or 100% do not necessarily mean that a team has
71 | been mathematically eliminated or clinched a playoff spot.
72 |
73 |
81 |
82 | Note that for this league, {playoffTeams} teams make the
83 | playoffs.
84 |
85 |
86 |
87 | All percentages are based on simulation results alone.
88 | Values of 0% or 100% do not necessarily mean that a team has
89 | been mathematically eliminated or clinched a playoff spot.
90 |
91 |
73 | Don't see your league? Add it manually by entering your league's
74 | details below.
75 |
76 |
124 | >
125 | );
126 | };
127 |
128 | export default NewLeagueForm;
129 |
--------------------------------------------------------------------------------
/frontend/src/utils/api.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const api = axios.create({
4 | baseURL: process.env.REACT_APP_API_BASE_URL,
5 | });
6 |
7 | export const safeFetch = async (
8 | endpoint,
9 | options,
10 | verbose = false,
11 | retry = 1 // retry = 1 means it will make 2 attempts
12 | ) => {
13 | let attempts = 0;
14 |
15 | while (attempts < retry + 1) {
16 | if (verbose) {
17 | console.log("=====================================");
18 | console.log(
19 | `[safeFetch] Attempt ${attempts + 1} for endpoint: ${endpoint}`
20 | );
21 | console.log("[safeFetch] Options:", options);
22 | }
23 |
24 | let response;
25 | let data;
26 |
27 | try {
28 | response = await fetch(endpoint, options);
29 | if (verbose) {
30 | console.log(`[safeFetch] Received response:`, response);
31 | }
32 |
33 | // Attempt to parse JSON
34 | try {
35 | data = await response.json();
36 | if (verbose) {
37 | console.log("[safeFetch] Parsed JSON data:", data);
38 | }
39 | } catch (jsonErr) {
40 | if (verbose) {
41 | console.error("[safeFetch] Failed to parse JSON:", jsonErr);
42 | }
43 | throw new Error("Failed to parse JSON response");
44 | }
45 |
46 | if (verbose) {
47 | console.log(`Data code: ${data?.code}`);
48 | }
49 | if (response.status === 409 && data.code === "too_soon_league") {
50 | return {
51 | redirect: `/fantasy_stats/uh-oh-too-early/league-homepage/${data.leagueYear}/${data.leagueId}`,
52 | };
53 | } else if (
54 | response.status === 409 &&
55 | data.code === "too_soon_simulations"
56 | ) {
57 | return {
58 | redirect: `/fantasy_stats/uh-oh-too-early/playoff-simulations/${data.leagueYear}/${data.leagueId}`,
59 | };
60 | } else if (
61 | response.status === 400 &&
62 | data.code === "invalid_league"
63 | ) {
64 | return { redirect: "/fantasy_stats/invalid-league" };
65 | }
66 |
67 | if (!response.ok) {
68 | if (verbose) {
69 | console.error(
70 | `[safeFetch] Unexpected response status: ${response.status}`,
71 | data
72 | );
73 | }
74 | throw new Error(
75 | `Unexpected API response: ${response.status} ${
76 | data?.message || ""
77 | }`
78 | );
79 | }
80 |
81 | if (verbose) {
82 | console.log("[safeFetch] Successful response, returning data.");
83 | }
84 | return data;
85 | } catch (err) {
86 | if (verbose) {
87 | console.error(
88 | `[safeFetch] Attempt ${attempts + 1} failed:`,
89 | err
90 | );
91 | }
92 |
93 | attempts += 1;
94 |
95 | if (attempts >= retry + 1) {
96 | if (verbose) {
97 | console.error("[safeFetch] All retry attempts failed.");
98 | }
99 | throw err; // Let ErrorBoundary or caller handle
100 | }
101 |
102 | if (verbose) {
103 | console.log("[safeFetch] Retrying...");
104 | }
105 | } finally {
106 | if (verbose) {
107 | console.log(
108 | `[safeFetch] Finished attempt ${attempts} for endpoint: ${endpoint}`
109 | );
110 | console.log("=====================================");
111 | }
112 | }
113 | }
114 | };
115 |
116 | export const fetchWithRetry = async (url, options, retries) => {
117 | for (let i = 0; i <= retries; i++) {
118 | try {
119 | const response = await fetch(url, options);
120 | // If we get a response, return it (even if it's a 400/500)
121 | return response;
122 | } catch (err) {
123 | // Only retry on network errors
124 | if (i === retries) {
125 | throw err;
126 | }
127 | // Otherwise, continue to next retry
128 | }
129 | }
130 | };
131 |
--------------------------------------------------------------------------------
/backend/src/doritostats/scrape_player_stats.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from typing import List
3 |
4 | import pandas as pd
5 | from espn_api.football import League, Team, Player
6 | from backend.src.doritostats.fetch_utils import fetch_league
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 |
11 | def extract_player_stats(
12 | team: Team, team_lineup: List[Player], week: int
13 | ) -> pd.DataFrame:
14 | df = pd.DataFrame()
15 | for i, player in enumerate(team_lineup):
16 | player_data = {
17 | "week": week,
18 | "team_owner": team.owner,
19 | "team_name": team.team_name,
20 | "team_division": team.division_name,
21 | "player_name": player.name,
22 | "player_id": player.playerId,
23 | "position_rank": player.posRank, # Empty?
24 | "eligible_slots": player.eligibleSlots,
25 | "acquisition_type": player.acquisitionType, # Empty?
26 | "pro_team": player.proTeam,
27 | "current_team_id": player.onTeamId, # The current ESPN fantasy team ID that the player is on (only accurate at time of query)
28 | "player_position": player.position,
29 | "player_active_status": player.active_status,
30 | "stats": player.stats,
31 | }
32 |
33 | # Add stat to player_data
34 | if week in player.stats.keys() and player.active_status != "bye":
35 | player_data["player_points_week"] = player.stats[week]["points"]
36 | player_data["player_percent_owned"] = player.percent_owned
37 | player_data["player_percent_started"] = player.percent_started
38 | player_data["player_total_points"] = player.total_points
39 | player_data["player_projected_total_points"] = player.projected_total_points
40 | player_data["player_avg_points"] = player.avg_points
41 | player_data["player_projected_avg_points"] = player.projected_avg_points
42 | else:
43 | player_data["player_points_week"] = 0
44 | player_data["player_percent_owned"] = 0
45 | player_data["player_percent_started"] = 0
46 | player_data["player_total_points"] = 0
47 | player_data["player_projected_total_points"] = 0
48 | player_data["player_avg_points"] = 0
49 | player_data["player_projected_avg_points"] = 0
50 |
51 | if 0 in player.stats.keys():
52 | player_data["player_points_season"] = player.stats[0]["points"]
53 | else:
54 | player_data["player_points_season"] = 0
55 |
56 | df = pd.concat([df, pd.Series(player_data).to_frame().T])
57 |
58 | return df
59 |
60 |
61 | def get_stats_by_matchup(
62 | league_id: int, year: int, swid: str, espn_s2: str
63 | ) -> pd.DataFrame:
64 | """This function creates a historical dataframe for the league in a given year.
65 | The data is based on player-level stats, and is organized by week and matchup.
66 |
67 | It generates this dataframe by:
68 | - For each week that has elapsed, get the BoxScores for that week:
69 | - For each Matchup in the BoxScores:
70 | Grab each stat by looking at the Matchup.home_team, Matchup.home_lineup, Matchup.away_team, and Matchup.away_lineup
71 |
72 | This is used for years in 2019 or later, where the BoxScores are available.
73 |
74 | Args:
75 | league_id (int): League ID
76 | year (int): Year of the league
77 | swid (str): User credential
78 | espn_s2 (str): User credential
79 |
80 | Returns:
81 | pd.DataFrame: Historical player stats dataframe
82 | """
83 | # Fetch league for year
84 | league = fetch_league(league_id=league_id, year=year, swid=swid, espn_s2=espn_s2)
85 |
86 | # Instantiate data frame
87 | df = pd.DataFrame()
88 |
89 | # Loop through each week that has happened
90 | current_matchup_period = league.settings.week_to_matchup_period[league.current_week]
91 | for week in range(current_matchup_period):
92 | league.load_roster_week(week + 1)
93 | box_scores = league.box_scores(week + 1)
94 |
95 | # Instantiate week data frame
96 | df_week = pd.DataFrame()
97 | for i, matchup in enumerate(box_scores):
98 | # Skip byes
99 | if (type(matchup.home_team) != Team) or (type(matchup.away_team) != Team):
100 | continue
101 |
102 | # Get stats for home team
103 | df_home_team = extract_player_stats(
104 | matchup.home_team, matchup.home_lineup, week + 1
105 | )
106 |
107 | # Get stats for away team
108 | df_away_team = extract_player_stats(
109 | matchup.away_team, matchup.away_lineup, week + 1
110 | )
111 |
112 | # Append to week data frame
113 | df_week = pd.concat([df_week, df_home_team, df_away_team])
114 |
115 | df = pd.concat([df, df_week])
116 |
117 | df["league_id"] = league_id
118 | df["year"] = year
119 |
120 | return df
121 |
--------------------------------------------------------------------------------
/backend/fantasy_stats/errors/email.py:
--------------------------------------------------------------------------------
1 | import html
2 | import json
3 | import os
4 | import traceback
5 |
6 | import resend
7 | import pandas as pd
8 |
9 | from backend.src.doritostats.fetch_utils import get_postgres_conn
10 | from backend.fantasy_stats.models import LeagueInfo
11 |
12 |
13 | def send_new_league_added_alert(league_info: LeagueInfo):
14 | # Load environment variables
15 | sender_email = os.getenv("SENDER_EMAIL")
16 | recipient_email = os.getenv("RECIPIENT_EMAIL")
17 | resend.api_key = os.getenv("RESEND_API_KEY")
18 |
19 | # Turn these into plain/html MIMEText objects
20 | league_name = league_info.league_name
21 | league_id = league_info.league_id
22 | year = league_info.league_year
23 |
24 | # Build message
25 | with get_postgres_conn() as conn:
26 | n_leagues_2025 = pd.read_sql(
27 | "SELECT COUNT(*) FROM public.fantasy_stats_leagueinfo WHERE league_year = 2025 and not deleted",
28 | conn,
29 | ).values[0][0]
30 | n_leagues_added_this_year = pd.read_sql(
31 | "SELECT COUNT(*) FROM public.fantasy_stats_leagueinfo WHERE not deleted and created_date > '2025-03-15'",
32 | conn,
33 | ).values[0][0]
34 | n_leagues_added_all_time = pd.read_sql(
35 | "SELECT COUNT(*) FROM public.fantasy_stats_leagueinfo WHERE not deleted",
36 | conn,
37 | ).values[0][0]
38 |
39 | # Fill in the placeholders in the email template
40 | body = f"""
41 |
42 |
43 |
🎉🎉 A new league was added! 🎉🎉
44 |
45 | League name: {league_name} ({league_id})
46 | League year: {year}
47 | Date added: {pd.Timestamp.now(tz='US/Eastern').strftime('%B %d, %Y @ %I:%M %p')}
48 |
49 | Total 2025 season leagues added: {n_leagues_2025}
50 | Total leagues added this year: {n_leagues_added_this_year}
51 | Total leagues added all time: {n_leagues_added_all_time}
52 |
93 | Click the buttons below to simulate different errors. If your
94 | ErrorBoundary is working, you should see your custom error page
95 | instead of a green screen.
96 |