From 112d1ab775e8acd98fb9e96c51432cd7d9c80984 Mon Sep 17 00:00:00 2001
From: uberfastman <4575707+uberfastman@users.noreply.github.com>
Date: Sat, 25 Nov 2023 01:40:00 -0500
Subject: [PATCH] v17.0.0 change app to use pydantic and .env files instead of
config.ini files for settings, fix cbs leagues without FAAB, upgrade Docker
to Python 3.11.6, upgrade dependencies and documentation
---
.dockerignore | 5 +-
.github/ISSUE_TEMPLATE/bug-report.md | 2 +-
.github/ISSUE_TEMPLATE/feature-request.md | 2 +-
.gitignore | 7 +-
.venv | 2 +-
README.md | 946 +++++++++++++---------
calculate/bad_boy_stats.py | 4 +-
calculate/beef_stats.py | 4 +-
calculate/coaching_efficiency.py | 9 +-
calculate/covid_risk.py | 298 -------
calculate/metrics.py | 24 +-
calculate/playoff_probabilities.py | 26 +-
calculate/points_by_position.py | 2 +-
calculate/season_averages.py | 4 +-
compose.build.yaml | 1 +
compose.yaml | 4 +-
config.template.ini | 118 ---
dao/base.py | 19 +-
dao/platforms/__init__.py | 0
dao/platforms/base/base.py | 10 +-
dao/platforms/cbs.py | 15 +-
dao/platforms/espn.py | 15 +-
dao/platforms/fleaflicker.py | 17 +-
dao/platforms/sleeper.py | 9 +-
dao/platforms/yahoo.py | 17 +-
docker/Dockerfile | 3 +-
integrations/drive_integration.py | 38 +-
integrations/slack_integration.py | 26 +-
main.py | 64 +-
report/builder.py | 63 +-
report/constants.py | 17 -
report/data.py | 26 +-
report/pdf/charts/bar.py | 4 +-
report/pdf/charts/line.py | 2 +-
report/pdf/charts/pie.py | 2 +-
report/pdf/generator.py | 269 +++---
requirements.txt | 20 +-
resources/documentation/README.md | 4 +-
resources/documentation/descriptions.py | 8 -
resources/espn_quickstart.py | 4 +-
resources/files/crime_categories.json | 2 +-
resources/google_quickstart.py | 2 +-
tests/test_features.py | 38 +-
utilities/app.py | 263 +-----
utilities/config.py | 136 ----
utilities/constants.py | 42 +
{report => utilities}/logger.py | 19 +-
utilities/settings.py | 475 +++++++++++
utilities/utils.py | 11 +-
49 files changed, 1450 insertions(+), 1648 deletions(-)
delete mode 100644 calculate/covid_risk.py
delete mode 100644 config.template.ini
delete mode 100644 dao/platforms/__init__.py
delete mode 100644 report/constants.py
delete mode 100644 utilities/config.py
create mode 100644 utilities/constants.py
rename {report => utilities}/logger.py (92%)
create mode 100644 utilities/settings.py
diff --git a/.dockerignore b/.dockerignore
index ed50e2ed..eddcb0c2 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -9,6 +9,7 @@
**.pytest_cache
# environment
+.env*
.venv
# auth secrets
@@ -19,10 +20,6 @@
**credentials*.json
!**credentials.template.json
-# configuration
-config*.ini
-!config.template.ini
-
# output
**logs/
output/
diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md
index acace776..3172b36e 100644
--- a/.github/ISSUE_TEMPLATE/bug-report.md
+++ b/.github/ISSUE_TEMPLATE/bug-report.md
@@ -22,4 +22,4 @@ XXXXXX...
macOS/Windows/Linux
#### Other
-Any other setup/environment/configuration information you might deem potentially relevant.
+Any other setup/environment/settings information you might deem potentially relevant.
diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md
index b8c59d8e..474c079d 100644
--- a/.github/ISSUE_TEMPLATE/feature-request.md
+++ b/.github/ISSUE_TEMPLATE/feature-request.md
@@ -11,4 +11,4 @@ assignees: ''
Please describe what new idea/feature would like to see added to the app.
## Use case
-Please elaborate on how your idea/feature request will be provide additional value and/or interesting information beyond what is already included in the generated reports.
+Please elaborate on how your idea/feature request will provide additional value and/or interesting information beyond what is already included in the generated reports.
diff --git a/.gitignore b/.gitignore
index dce08f02..f3590f37 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,7 +9,10 @@
**.pytest_cache
# environment
+.env*
+env*
.venv
+venv
# auth secrets
**private*.json
@@ -19,10 +22,6 @@
**credentials*.json
!**credentials.template.json
-# configuration
-config*.ini
-!config.template.ini
-
# output
**logs/
output/
diff --git a/.venv b/.venv
index f81b7f7c..17e44014 100644
--- a/.venv
+++ b/.venv
@@ -1 +1 @@
-fantasy-football-metrics-weekly-report
+fantasy-football-metrics-weekly-report-python_3.11.6
diff --git a/README.md b/README.md
index 0553fb25..6a50d583 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@
➡️
[![Web App](https://img.shields.io/github/milestones/progress-percent/uberfastman/fantasy-football-metrics-weekly-report/3)](https://github.com/uberfastman/fantasy-football-metrics-weekly-report/milestone/3)
[![Multi-Platform Support](https://img.shields.io/github/milestones/progress-percent/uberfastman/fantasy-football-metrics-weekly-report/2)](https://github.com/uberfastman/fantasy-football-metrics-weekly-report/milestone/2)
-[![Metrics](https://img.shields.io/github/milestones/progress-percent/uberfastman/fantasy-football-metrics-weekly-report/4)](https://github.com/uberfastman/fantasy-football-metrics-weekly-report/milestone/4)
+[![Metrics](https://img.shields.io/github/milestones/progress-percent/uberfastman/fantasy-football-metrics-weekly-report/4)](https://github.com/uberfastman/fantasy-football-metrics-weekly-report/milestone/4)
[![Maintenance](https://img.shields.io/github/milestones/progress-percent/uberfastman/fantasy-football-metrics-weekly-report/5)](https://github.com/uberfastman/fantasy-football-metrics-weekly-report/milestone/5)
[![Completed Milestones](https://img.shields.io/github/milestones/closed/uberfastman/fantasy-football-metrics-weekly-report)](https://github.com/uberfastman/fantasy-football-metrics-weekly-report/milestones?state=closed)
@@ -47,39 +47,41 @@
---
### Table of Contents
+
* ***[Quickstart Guide](#quickstart-guide)***
* [About](#about)
- * [Example Report](#example-report)
- * [Updating](#updating)
+ * [Example Report](#example-report)
+ * [Updating](#updating)
* [Dependencies](#dependencies)
* [Setup](#setup)
- * [Command-line](#command-line)
- * [Git](#git)
- * [Docker](#docker)
- * [GitHub](#github)
- * [Platforms](#platforms)
- * [Yahoo Setup](#yahoo-setup)
- * [Fleaflicker Setup](#fleaflicker-setup)
- * [Sleeper Setup](#sleeper-setup)
- * [ESPN Setup](#espn-setup)
- * [CBS Setup](#cbs-setup)
+ * [Command-line](#command-line)
+ * [Git](#git)
+ * [Docker](#docker)
+ * [GitHub](#github)
+ * [Platforms](#platforms)
+ * [Yahoo Setup](#yahoo-setup)
+ * [Fleaflicker Setup](#fleaflicker-setup)
+ * [Sleeper Setup](#sleeper-setup)
+ * [ESPN Setup](#espn-setup)
+ * [CBS Setup](#cbs-setup)
* [Running the Report Application](#running-the-report-application)
-* [Configuration](#configuration)
- * [Report Features](#report-features)
- * [Report Formatting](#report-formatting)
- * [Report Settings](#report-settings)
+* [Settings](#settings)
+ * [Report Features](#report-features)
+ * [Report Formatting](#report-formatting)
+ * [Report Settings](#report-settings)
* [Usage](#usage)
* [Additional Integrations](#additional-integrations)
- * [Google Drive](#google-drive-setup)
- * [Slack](#slack-setup)
+ * [Google Drive](#google-drive-setup)
+ * [Slack](#slack-setup)
* [Troubleshooting](#troubleshooting)
- * [Logs](#logs)
- * [Yahoo](#yahoo)
- * [Docker on Windows](#docker-on-windows)
+ * [Logs](#logs)
+ * [Yahoo](#yahoo)
+ * [Docker on Windows](#docker-on-windows)
---
+
## *Quickstart Guide*
```diff
@@ -88,29 +90,31 @@
1. Open a command-line interface (see the [Command-line](#command-line) section for more details) on your computer.
- * ***macOS***: type `Cmd + Space` (`⌘ + Space`) to bring up Spotlight, and search for "Terminal" and hit enter).
-
- * ***Windows***: type `Windows + R` to open the "Run" box, then type `cmd` and then click "OK" to open a regular Command Prompt (or type `cmd` and then press `Ctrl + Shift + Enter` to open a Command Prompt as administrator.)
-
- * ***Linux (Ubuntu)***: type `Ctrl+Alt+T`.
+ * ***macOS***: type `Cmd + Space` (`⌘ + Space`) to bring up Spotlight, and search for "Terminal" and hit enter).
+
+ * ***Windows***: type `Windows + R` to open the "Run" box, then type `cmd` and then click "OK" to open a regular
+ Command Prompt (or type `cmd` and then press `Ctrl + Shift + Enter` to open a Command Prompt as administrator.)
+
+ * ***Linux (Ubuntu)***: type `Ctrl+Alt+T`.
2. Install Git (see [Git](#git) section for more details).
- * ***macOS***: run `git --version` for the first time, and when prompted, install the *Xcode Command Line Tools*.
-
- * ***Windows***: Download [Git for Windows](https://git-scm.com/download/win) and install it.
-
- * ***Linux (Ubuntu)***: `sudo apt install git-all` (see above link for different Linux distributions)
+ * ***macOS***: run `git --version` for the first time, and when prompted, install the *Xcode Command Line Tools*.
+
+ * ***Windows***: Download [Git for Windows](https://git-scm.com/download/win) and install it.
+
+ * ***Linux (Ubuntu)***: `sudo apt install git-all` (see above link for different Linux distributions)
3. Install [Docker Desktop](#docker) for your operating system.
- * ***macOS***: [Docker Desktop for Mac](https://docs.docker.com/docker-for-mac/install/)
-
- * ***Windows***: [Docker Desktop for Windows](https://docs.docker.com/docker-for-windows/install/)
-
- * ***Linux***: [Docker for Linux](https://docs.docker.com/engine/install/)
+ * ***macOS***: [Docker Desktop for Mac](https://docs.docker.com/docker-for-mac/install/)
+
+ * ***Windows***: [Docker Desktop for Windows](https://docs.docker.com/docker-for-windows/install/)
+
+ * ***Linux***: [Docker for Linux](https://docs.docker.com/engine/install/)
-4. Clone this app from GitHub (see [GitHub](#github) section for more details) to wherever you would like to store the app code on your computer (I recommend something like your user Documents folder).
+4. Clone this app from GitHub (see [GitHub](#github) section for more details) to wherever you would like to store the
+ app code on your computer (I recommend something like your user Documents folder).
```bash
git clone https://github.com/uberfastman/fantasy-football-metrics-weekly-report.git
```
@@ -119,47 +123,53 @@
```bash
cd fantasy-football-metrics-weekly-report
```
-
-6. Follow the required setup instructions for whichever fantasy football platform you use: [Yahoo](#yahoo-setup), [ESPN](#espn-setup), [Sleeper](#sleeper-setup), or [Fleaflicker](#fleaflicker-setup)
-
-7. Configure the app by updating values in the `config.ini` file (see the [Configuration](#configuration) section for more details).
-
- * *Alternately, the first time you try running the app it will detect that you have no `config.ini` file, and will ask you if you wish to create one. Provide values for the remaining prompts (it will ask you for your* **fantasy football platform**, *your* **league ID**, *the* **NFL season (year)**, *and the* **current NFL week**, *so have those values ready.*
-
-8. Run the Fantasy Football Metrics Weekly Report app using Docker (see the [Running the Report Application](#running-the-report-application) section for more details).
-
- If on Windows, see the [Docker on Windows](#docker-on-windows) troubleshooting section if you encounter any permissions or access issues.
-
- * Run:
- ```bash
- docker compose up -d
- ```
- If you wish to see the Docker logs, then run `docker compose up`.
-
- * Wait for the above command to complete, then run:
- ```bash
- docker exec -it fantasy-football-metrics-weekly-report-app-1 python main.py
- ```
-
- * ***Follow the prompts to generate a report for your fantasy league!***
-
+
+6. Follow the required setup instructions for whichever fantasy football platform you
+ use: [Yahoo](#yahoo-setup), [ESPN](#espn-setup), [Sleeper](#sleeper-setup), or [Fleaflicker](#fleaflicker-setup)
+
+7. Update the values in the `.env` file (see the [Settings](#settings) section for more details).
+
+ * *Alternately, the first time you try running the app it will detect that you have no `.env` file, and will ask you if you wish to create one. Provide values for the remaining prompts (it will ask you for your* **fantasy football platform**, *your* **league ID**, *the* **NFL season (year)**, *and the* **current NFL week**, *so have those values ready.*
+
+8. Run the Fantasy Football Metrics Weekly Report app using Docker (see
+ the [Running the Report Application](#running-the-report-application) section for more details).
+
+ If on Windows, see the [Docker on Windows](#docker-on-windows) troubleshooting section if you encounter any
+ permissions or access issues.
+
+ * Run:
+ ```bash
+ docker compose up -d
+ ```
+ If you wish to see the Docker logs, then run `docker compose up`.
+
+ * Wait for the above command to complete, then run:
+ ```bash
+ docker exec -it fantasy-football-metrics-weekly-report-app-1 python main.py
+ ```
+
+ * ***Follow the prompts to generate a report for your fantasy league!***
+
---
+
### About
-The Fantasy Football Metrics Weekly Report application automatically generates a report in the form of a PDF file that contains a host of metrics and rankings for teams in a given fantasy football league.
+
+The Fantasy Football Metrics Weekly Report application automatically generates a report in the form of a PDF file that
+contains a host of metrics and rankings for teams in a given fantasy football league.
Currently supported fantasy football platforms:
* **Yahoo**
-
+
* **Fleaflicker**
* **Sleeper**
* **ESPN**
-* **CBS***
+* **CBS**
*Platforms in development:*
@@ -170,51 +180,71 @@ Currently supported fantasy football platforms:
* ***MyFantasyLeague***
+
#### Example Report
+
***You can see an example of what a report looks like [here](resources/files/example_report.pdf)!***
+
#### Updating
-Every time you run the app it will check to see if you are using the latest version (as long as you have an active network connection). If it detects that your app is out of date, you will see prompt asking you if you wish to update the app. Type `y` and hit enter to confirm.
+Every time you run the app it will check to see if you are using the latest version (as long as you have an active
+network connection). If it detects that your app is out of date, you will see prompt asking you if you wish to update
+the app. Type `y` and hit enter to confirm.
-If you wish to update the app yourself manually, you can just type `n` to skip automatically updating, and run `git pull origin main` manually from within the application directory on the command line.
+If you wish to update the app yourself manually, you can just type `n` to skip automatically updating, and
+run `git pull origin main` manually from within the application directory on the command line.
---
+
### Dependencies
-The application is actively developed in macOS, but is cross-platform compatible. The app requires ***Python 3.9 or later*** (Python 2 is no longer supported). To check if you have the minimum required version of Python or later installed, open up a window in Terminal (macOS), Command Prompt (Windows), or a command line shell of your choice, and run `python --version`. If the return value is `Python 3.x.x` where the first `x` is equal to or greater than the minimum required minor version, you are good to go. If the return is `Python 2.x.x`, you will need to install the correct Python 3 version. Check out the instructions [here](https://realpython.com/installing-python/) for how to install Python 3 on your system.
+The application is actively developed in macOS, but is cross-platform compatible. The app requires
+***Python 3.9 or later*** (Python 2 is no longer supported). To check if you have the minimum required version of Python
+or later installed, open up a window in Terminal (macOS), Command Prompt (Windows), or a command line shell of your
+choice, and run `python --version`. If the return value is `Python 3.x.x` where the first `x` is equal to or greater
+than the minimum required minor version, you are good to go. If the return is `Python 2.x.x`, you will need to install
+the correct Python 3 version. Check out the instructions [here](https://realpython.com/installing-python/) for how to
+install Python 3 on your system.
Project dependencies can be viewed in the [`requirements.txt`](requirements.txt) file.
---
+
### Setup*
-The Fantasy Football Metrics Weekly Report requires several sets of setup steps, depending on which platform(s) for which you will be running it. To get the application running locally, you will first need to complete the below setup.
+The Fantasy Football Metrics Weekly Report requires several sets of setup steps, depending on which platform(s) for
+which you will be running it. To get the application running locally, you will first need to complete the below setup.
-_\* General setup **excludes** Google Drive and Slack integrations. See [Additional Integrations](#additional-integrations) for details on including those add-ons._
+_\* General setup **excludes** Google Drive and Slack integrations.
+See [Additional Integrations](#additional-integrations) for details on including those add-ons._
---
+
#### Command-line
Open a command-line terminal/prompt:
* ***macOS***: type `Cmd + Space` (`⌘ + Space`) to bring up Spotlight, and search for "Terminal" and hit enter).
-* ***Windows***: type `Windows + R` to open the "Run" box, then type `cmd` and then click "OK" to open a regular Command Prompt (or type `cmd` and then press `Ctrl + Shift + Enter` to open a Command Prompt as administrator.)
+* ***Windows***: type `Windows + R` to open the "Run" box, then type `cmd` and then click "OK" to open a regular Command
+ Prompt (or type `cmd` and then press `Ctrl + Shift + Enter` to open a Command Prompt as administrator.)
* ***Linux (Ubuntu)***: type `Ctrl+Alt+T`.
+
#### Git
-Install `git` (if you do not already have it installed). You can see detailed instructions for installation on your OS [here](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git).
+Install `git` (if you do not already have it installed). You can see detailed instructions for installation on your
+OS [here](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git).
* ***macOS***: run `git --version` for the first time, and when prompted, install the *Xcode Command Line Tools*.
@@ -222,17 +252,23 @@ Install `git` (if you do not already have it installed). You can see detailed in
* ***Linux (Ubuntu)***: `sudo apt install git-all` (see above link for different Linux distributions)
-**NOTE**: If you are comfortable using the command line, feel free to just install `git` for the command line. *However*, if using the command line is not something you have much experience with and would prefer to do less in a command line shell, you can install [Git for Desktop](https://desktop.github.com).
+**NOTE**: If you are comfortable using the command line, feel free to just install `git` for the command line.
+*However*, if using the command line is not something you have much experience with and would prefer to do less in a
+command line shell, you can install [Git for Desktop](https://desktop.github.com).
-***If you use the Git for Desktop client, please keep the project name the same*** (`fantasy-football-metrics-weekly-report`) ***or else you will run into additional issues when running the app!***
+***If you use the Git for Desktop client, please keep the project name the same
+*** (`fantasy-football-metrics-weekly-report`)
+***or else you will run into additional issues when running the app!***
+
#### Docker
-* **NOTE: This application requires you to have administrative (admin) access for the computer on which you are installing it.**
+* **NOTE: This application requires you to have administrative (admin) access for the computer on which you are
+ installing it.**
Install [Docker](https://docs.docker.com/get-docker/) for your operating system:
-
+
* ***macOS***: [Docker Desktop for Mac](https://docs.docker.com/docker-for-mac/install/)
* ***Windows***: [Docker Desktop for Windows](https://docs.docker.com/docker-for-windows/install/)
@@ -240,6 +276,7 @@ Install [Docker](https://docs.docker.com/get-docker/) for your operating system:
* ***Linux***: [Docker for Linux](https://docs.docker.com/engine/install/)
+
#### GitHub
Clone this project to whichever directory you wish to use for this app:
@@ -249,7 +286,8 @@ Clone this project to whichever directory you wish to use for this app:
git clone https://github.com/uberfastman/fantasy-football-metrics-weekly-report.git
```
-* If you already have an account on [GitHub](https://github.com), then I recommend using [SSH to connect with GitHub](https://help.github.com/en/articles/connecting-to-github-with-ssh) by running:
+* If you already have an account on [GitHub](https://github.com), then I recommend
+ using [SSH to connect with GitHub](https://help.github.com/en/articles/connecting-to-github-with-ssh) by running:
```bash
git clone git@github.com:uberfastman/fantasy-football-metrics-weekly-report.git
```
@@ -257,373 +295,485 @@ Clone this project to whichever directory you wish to use for this app:
---
+
#### Platforms
+
##### Yahoo Setup
-Yahoo Fantasy Sports has a public API documented [here](https://developer.yahoo.com/fantasysports/guide/). The Fantasy Football Metrics Weekly Report application uses my own [YFPY](https://yfpy.uberfastman.com) Python wrapper around this API to retrieve the necessary data to generate reports.
+Yahoo Fantasy Sports has a public API documented [here](https://developer.yahoo.com/fantasysports/guide/). The Fantasy
+Football Metrics Weekly Report application uses my own [YFPY](https://yfpy.uberfastman.com) Python wrapper around this
+API to retrieve the necessary data to generate reports.
1. Log in to a Yahoo account with access to whatever fantasy football leagues from which you wish to retrieve data.
-2. Retrieve your Yahoo Fantasy football league id, which you can find by going to [https://football.fantasysports.yahoo.com](https://football.fantasysports.yahoo.com), clicking on your league, and looking here:
-
- ![yahoo-fantasy-football-league-id-location.png](resources/images/yahoo-fantasy-football-league-id-location.png)
-
-3. Change the `league_id` value in `config.ini` to the above located league id.
-
-4. Go to [https://developer.yahoo.com/apps/create/](https://developer.yahoo.com/apps/create/) and create an app (you must be logged into your Yahoo account as stated above). For the app, select the following options:
-
- 1. `Application Name` (**Required**): `yahoo fantasy sports metrics` (you can name your app whatever you want, but this is just an example).
-
- 2. `Application Type` (**Required**): select the `Installed Application` radio button.
-
- 3. `Description` (*Optional*): you *may* write a description of what the app does.
-
- 4. `Home Page URL` (*Optional*): if you have a web address related to your app you *may* add it here.
-
- 5. `Redirect URI(s)` (**Required**): this field must contain a valid redirect address, so you can use `localhost:8080`
-
- 6. `API Permissions` (**Required**): check the `Fantasy Sports` checkbox. You can leave the `Read` option selected (appears in an accordion expansion underneath the `Fantasy Sports` checkbox once you select it).
-
+2. Retrieve your Yahoo Fantasy football league id, which you can find by going
+ to [https://football.fantasysports.yahoo.com](https://football.fantasysports.yahoo.com), clicking on your league, and
+ looking here:
+
+ ![yahoo-fantasy-football-league-id-location.png](resources/images/yahoo-fantasy-football-league-id-location.png)
+
+3. Change the `LEAGUE_ID` value in your `.env` file to the above located league id.
+
+4. Go to [https://developer.yahoo.com/apps/create/](https://developer.yahoo.com/apps/create/) and create an app (you
+ must be logged into your Yahoo account as stated above). For the app, select the following options:
+
+ 1. `Application Name` (**Required**): `yahoo fantasy sports metrics` (you can name your app whatever you want, but
+ this is just an example).
+
+ 2. `Application Type` (**Required**): select the `Installed Application` radio button.
+
+ 3. `Description` (*Optional*): you *may* write a description of what the app does.
+
+ 4. `Home Page URL` (*Optional*): if you have a web address related to your app you *may* add it here.
+
+ 5. `Redirect URI(s)` (**Required**): this field must contain a valid redirect address, so you can
+ use `localhost:8080`
+
+ 6. `API Permissions` (**Required**): check the `Fantasy Sports` checkbox. You can leave the `Read` option selected (
+ appears in an accordion expansion underneath the `Fantasy Sports` checkbox once you select it).
+
5. Click the `Create App` button.
-
-6. Once the app is created, it should redirect you to a page for your app, which will show both a `Client ID` and a `Client Secret`.
-
-7. Copy the file `private.template.json` (located in the `auth/yahoo/` directory), and rename the file copy `private.json` by running the below command in your command line shell:
-
- * **macOS**/**Linux**: `cp auth/yahoo/private.template.json private.json auth/yahoo/private.json`
-
- * **Windows**: `copy auth\yahoo\private.template.json auth\yahoo\private.json`
-
-8. Open your new `private.json` file with your preferred text editor (such as TexEdit in macOS or Notepad in Windows), then copy and paste the `Client ID` and `Client Secret` values from your above created Yahoo app to their respective fields (make sure the strings are wrapped regular quotes (`""`), NOT formatted quotes (`“”`)). The path to this file will be needed to point the YFPY API wrapper responsible for data retrieval to your credentials.
-
-9. The first time you run the app, it will initialize the OAuth connection between the report generator and your Yahoo account.
-
-**NOTE**: ***If your Yahoo league uses FAAB (Free Agent Acquisition Budget) for player waivers, you must set the `initial_faab_budget` value in the `config.ini` file to reflect your league's starting budget, since this information does not seem to be available in the Yahoo API.***
+
+6. Once the app is created, it should redirect you to a page for your app, which will show both a `Client ID` and
+ a `Client Secret`.
+
+7. Copy the file `private.template.json` (located in the `auth/yahoo/` directory), and rename the file
+ copy `private.json` by running the below command in your command line shell:
+
+ * **macOS**/**Linux**: `cp auth/yahoo/private.template.json private.json auth/yahoo/private.json`
+
+ * **Windows**: `copy auth\yahoo\private.template.json auth\yahoo\private.json`
+
+8. Open your new `private.json` file with your preferred text editor (such as TexEdit in macOS or Notepad in Windows),
+ then copy and paste the `Client ID` and `Client Secret` values from your above created Yahoo app to their respective
+ fields (make sure the strings are wrapped regular quotes (`""`), NOT formatted quotes (`“”`)). The path to this file
+ will be needed to point the YFPY API wrapper responsible for data retrieval to your credentials.
+
+9. The first time you run the app, it will initialize the OAuth connection between the report generator and your Yahoo
+ account.
+
+**NOTE**:***If your Yahoo league uses FAAB (Free Agent Acquisition Budget) for player waivers, you must set
+the `YAHOO_INITIAL_FAAB_BUDGET` value in the `.env` file to reflect your league's starting budget, since this
+information does not seem to be available in the Yahoo API.***
##### You are now ready to [generate a report!](#running-the-report-application)
---
+
##### Fleaflicker Setup
-Fleaflicker has a public API documented [here](https://www.fleaflicker.com/api-docs/index.html). The Fantasy Football Metrics Weekly Report application uses this API to retrieve the necessary data to generate reports. ***Please note, some data required to provide certain information to the report is not currently available in the Fleaflicker API, so for the time being web-scraping is used to supplement the data gathered from the Fleaflicker API.***
+Fleaflicker has a public API documented [here](https://www.fleaflicker.com/api-docs/index.html). The Fantasy Football
+Metrics Weekly Report application uses this API to retrieve the necessary data to generate reports.
+***Please note, some data required to provide certain information to the report is not currently available in the
+Fleaflicker API, so for the time being web-scraping is used to supplement the data gathered from the Fleaflicker API.***
1. Retrieve your Fleaflicker league ID. You can find it by looking at the URL of your league in your browser:
- ![fleaflicker-fantasy-football-league-id-location.png](resources/images/fleaflicker-fantasy-football-league-id-location.png)
-
-2. Change the `league_id` value in `config.ini` to the above located league id.
+ ![fleaflicker-fantasy-football-league-id-location.png](resources/images/fleaflicker-fantasy-football-league-id-location.png)
-3. Make sure that you have accurately set the `season` configuration value in the `config.ini` file to reflect the desired year/season for which you are running the report application. This will ensure that the location of locally saved data is correct and API requests are properly formed.
+2. Change the `LEAGUE_ID` value in the `.env` file to the above located league id.
-4. You can also specify the `year` from the command line by running the report with the `-y ` flag.
+3. Make sure that you have accurately set the value of `SEASON` in the `.env` file to reflect the desired year/season
+ for which you are running the report application. This will ensure that the location of locally saved data is correct
+ and API requests are properly formed.
-5. Fleaflicker does not require any authentication to access their API at this time, so no additional steps are necessary.
+4. You can also specify the year/season from the command line by running the report with the `-y ` flag.
+
+5. Fleaflicker does not require any authentication to access their API at this time, so no additional steps are
+ necessary.
##### You are now ready to [generate a report!](#running-the-report-application)
---
+
##### Sleeper Setup
-Sleeper has a public API documented [here](https://docs.sleeper.app). The Fantasy Football Metrics Weekly Report application uses this API to retrieve the necessary data to generate reports. ***Please note, some data required to provide certain information to the report is not currently available in the Sleeper API, so a few small things are excluded in the report until such a time as the data becomes available***. That being said, the missing data does not fundamentally limit the capability of the app to generate a complete report.
+Sleeper has a public API documented [here](https://docs.sleeper.app). The Fantasy Football Metrics Weekly Report
+application uses this API to retrieve the necessary data to generate reports.
+***Please note, some data required to provide certain information to the report is not currently available in the
+Sleeper API, so a few small things are excluded in the report until such a time as the data becomes available***. That
+being said, the missing data does not fundamentally limit the capability of the app to generate a complete report.
1. Retrieve your Sleeper league ID. You can find it by looking at the URL of your league in your browser:
- ![sleeper-fantasy-football-league-id-location.png](resources/images/sleeper-fantasy-football-league-id-location.png)
-
-2. Change the `league_id` value in `config.ini` to the above located league id.
+ ![sleeper-fantasy-football-league-id-location.png](resources/images/sleeper-fantasy-football-league-id-location.png)
-3. *(Optional)* It is advised that you accurately set the `current_week` configuration value in the `config.ini` file to reflect the current/ongoing NFL week at the time of running the report, as the report will default to this value if retrieving the current NFL week from the Sleeper API (which the app uses to fetch the current NFL week) is unsuccessful.
+2. Change the `LEAGUE_ID` value in the `.env` file to the above located league id.
-4. Set
+3. *(Optional)* It is advised that you accurately set the `CURRENT_NFL_WEEK` value in the `.env` file to reflect the
+ current/ongoing NFL week at the time of running the report, as the report will default to this value if retrieving
+ the current NFL week from the Sleeper API (which the app uses to fetch the current NFL week) is unsuccessful.
##### You are now ready to [generate a report!](#running-the-report-application)
---
+
##### ESPN Setup
-ESPN has an undocumented public API which changed from v2 to v3 in 2018 and introduced some variance to its functionality, and as such is subject to unexpected and sudden changes. The Fantasy Football Metrics Weekly Report application uses this API to retrieve the necessary data to generate reports. ***Please note, some data required to provide certain information to the report is not currently available in the ESPN API, so a few small things are excluded in the report until such a time as the data becomes available***. That being said, the missing data does not fundamentally limit the capability of the app to generate a complete report.
+ESPN has an undocumented public API which changed from v2 to v3 in 2018 and introduced some variance to its
+functionality, and as such is subject to unexpected and sudden changes. The Fantasy Football Metrics Weekly Report
+application uses this API to retrieve the necessary data to generate reports.
+***Please note, some data required to provide certain information to the report is not currently available in the ESPN
+API, so a few small things are excluded in the report until such a time as the data becomes available***. That being
+said, the missing data does not fundamentally limit the capability of the app to generate a complete report.
1. Retrieve your ESPN league ID. You can find it by looking at the URL of your league in your browser:
- ![espn-fantasy-football-league-id-location.png](resources/images/espn-fantasy-football-league-id-location.png)
-
-2. Change the `league_id` value in `config.ini` to the above located league id.
-
-3. Make sure that you have accurately set the `season` configuration value in the `config.ini` file to reflect the desired year/season for which you are running the report application. This will ensure that the location of locally saved data is correct and API requests are properly formed.
-
-4. You can also specify the `year` from the command line by running the report with the `-y ` flag.
-
-5. Public ESPN leagues **do not** require any authentication to access their API at this time, so no additional steps are necessary for those leagues. However, certain data will not be available if you are not authenticated, so it is advised for you to still follow the below authentication steps anyway. For private leagues, you ***must*** complete one of the following authentication processes.
-
- 1. Allow the app to automatically retrieve your ESPN session cookies for you:
-
- 1. Copy the file `private.template.json` (located in the `auth/espn/` directory), and rename the file copy `private.json` by running the below command in your command line shell:
-
- * **macOS**/**Linux**: `cp auth/espn/private.template.json private.json auth/espn/private.json`
-
- * **Windows**: `copy auth\espn\private.template.json auth\espn\private.json`
-
- 2. Open your new `private.json` file with your preferred text editor (such as TexEdit in macOS or Notepad in Windows), ***delete*** the `swid` and `espn_s2` fields, and add your ESPN account username and password to their respective fields. ***Please note, the FFMWR app does not store any of your credentials, it simply uses them to log in to your account and obtain session cookies.*** The app uses [Selenium](https://www.selenium.dev) to run a headless browser to log in to ESPN on your behalf, so expect to see an email alerting you to a new device login. The Selenium process will retrieve your ESPN session cookies (`SWID` and `espn_s2`) and copy them into your `private.json` file for reuse with future ESPN API authentication.
-
- 2. Manually retrieve your ESPN session cookies:
-
- 1. Steven Morse has done a great deal of fantastic work to help teach people how to use the ESPN fantasy API, and has a useful blog post [here](https://stmorse.github.io/journal/espn-fantasy-3-python.html) detailing how to get your own session cookies. As stated in the aforementioned blog, you can get the cookies following the subsequent steps.
-
- 2. *"A lot of the ESPN Fantasy tools are behind a login-wall. Since accounts are free, this is not a huge deal, but becomes slightly annoying for GET requests because now we somehow need to “login” through the request. One way to do this is to send session cookies along with the request. Again this can take us into a gray area, but to my knowledge there is nothing prohibited about using your own cookies for personal use within your own league."*
-
- *Specifically, our GET request from the previous post is modified to look like, for example:*
- ```shell
- r = requests.get(
- 'https://games.espn.com/ffl/api/v2/scoreboard',
- params={'leagueId': 123456, 'seasonId': 2023, 'matchupPeriodId': 1},
- cookies={'swid': '{SWID-COOKIE-HERE}', 'espn_s2': 'LONG_ESPN_S2_COOKIE_HERE'}
- )
- ```
-
- *This should return the info you want even for a private league. I saw that the SWID and the ESPN_S2 cookies were the magic tickets based on the similar coding endeavors here and here and here.*
-
- *You can find these cookies in Safari by opening the Storage tab of Developer tools (you can turn on developer tools in preferences), and looking under espn.com in the Cookies folder. In Chrome, you can go to Preferences -> Advanced -> Content Settings -> Cookies -> See all cookies and site data, and looking for ESPN.*
-
- 3. Depending on what web browser (Firefox, Chrome, Edge, Brave, etc.) you are using, the process for viewing your session cookies in the web inspector will be different. I recommend Googling *"how to inspect element in [browser]"* (for your specific browser) to learn how to use that browser's web inspector.
-
- 4. Copy the file `private.template.json` (located in the `auth/espn/` directory), and rename the file copy `private.json` by running the below command in your command line shell:
-
- * **macOS**/**Linux**: `cp auth/espn/private.template.json private.json auth/espn/private.json`
-
- * **Windows**: `copy auth\espn\private.template.json auth\espn\private.json`
-
- 5. Open your new `private.json` file with your preferred text editor (such as TexEdit in macOS or Notepad in Windows), then copy and paste the above cookies into their respective fields. ***Please note, the `swid` will be surrounded by curly braces (`{...}`), which must be included.***
-
-**NOTE**: *Because ESPN made the change to their API between 2018 and 2019, ESPN support in the Fantasy Football Metrics Weekly Report application is currently limited to the 2019 season and later. Support for historical seasons will be implemented at a later time.
+ ![espn-fantasy-football-league-id-location.png](resources/images/espn-fantasy-football-league-id-location.png)
+
+2. Change the `LEAGUE_ID` value in the `.env` file to the above located league id.
+
+3. Make sure that you have accurately set the `SEASON` value in the `.env` file to reflect the desired year/season for
+ which you are running the report application. This will ensure that the location of locally saved data is correct and
+ API requests are properly formed.
+
+4. You can also specify the year/season from the command line by running the report with the `-y ` flag.
+
+5. Public ESPN leagues **do not** require any authentication to access their API at this time, so no additional steps
+ are necessary for those leagues. However, certain data will not be available if you are not authenticated, so it is
+ advised for you to still follow the below authentication steps anyway. For private leagues, you ***must*** complete
+ one of the following authentication processes.
+
+ 1. Allow the app to automatically retrieve your ESPN session cookies for you:
+
+ 1. Copy the file `private.template.json` (located in the `auth/espn/` directory), and rename the file
+ copy `private.json` by running the below command in your command line shell:
+
+ * **macOS**/**Linux**: `cp auth/espn/private.template.json private.json auth/espn/private.json`
+
+ * **Windows**: `copy auth\espn\private.template.json auth\espn\private.json`
+
+ 2. Open your new `private.json` file with your preferred text editor (such as TexEdit in macOS or Notepad in
+ Windows), ***delete*** the `swid` and `espn_s2` fields, and add your ESPN account username and password to
+ their respective fields.
+ ***Please note, the FFMWR app does not store any of your credentials, it simply uses them to log in to your
+ account and obtain session cookies.*** The app uses [Selenium](https://www.selenium.dev) to run a headless
+ browser to log in to ESPN on your behalf, so expect to see an email alerting you to a new device login. The
+ Selenium process will retrieve your ESPN session cookies (`SWID` and `espn_s2`) and copy them into
+ your `private.json` file for reuse with future ESPN API authentication.
+
+ 2. Manually retrieve your ESPN session cookies:
+
+ 1. Steven Morse has done a great deal of fantastic work to help teach people how to use the ESPN fantasy API,
+ and has a useful blog post [here](https://stmorse.github.io/journal/espn-fantasy-3-python.html) detailing how
+ to get your own session cookies. As stated in the aforementioned blog, you can get the cookies following the
+ subsequent steps.
+
+ 2. *"A lot of the ESPN Fantasy tools are behind a login-wall. Since accounts are free, this is not a huge deal,
+ but becomes slightly annoying for GET requests because now we somehow need to “login” through the request.
+ One way to do this is to send session cookies along with the request. Again this can take us into a gray
+ area, but to my knowledge there is nothing prohibited about using your own cookies for personal use within
+ your own league."*
+
+ *Specifically, our GET request from the previous post is modified to look like, for example:*
+ ```shell
+ r = requests.get(
+ 'https://games.espn.com/ffl/api/v2/scoreboard',
+ params={'leagueId': 123456, 'seasonId': 2023, 'matchupPeriodId': 1},
+ cookies={'swid': '{SWID-COOKIE-HERE}', 'espn_s2': 'LONG_ESPN_S2_COOKIE_HERE'}
+ )
+ ```
+
+ *This should return the info you want even for a private league. I saw that the SWID and the ESPN_S2 cookies
+ were the magic tickets based on the similar coding endeavors here and here and here.*
+
+ *You can find these cookies in Safari by opening the Storage tab of Developer tools (you can turn on
+ developer tools in preferences), and looking under espn.com in the Cookies folder. In Chrome, you can go to
+ Preferences -> Advanced -> Content Settings -> Cookies -> See all cookies and site data, and looking for
+ ESPN.*
+
+ 3. Depending on what web browser (Firefox, Chrome, Edge, Brave, etc.) you are using, the process for viewing
+ your session cookies in the web inspector will be different. I recommend Googling *"how to inspect element
+ in [browser]"* (for your specific browser) to learn how to use that browser's web inspector.
+
+ 4. Copy the file `private.template.json` (located in the `auth/espn/` directory), and rename the file
+ copy `private.json` by running the below command in your command line shell:
+
+ * **macOS**/**Linux**: `cp auth/espn/private.template.json private.json auth/espn/private.json`
+
+ * **Windows**: `copy auth\espn\private.template.json auth\espn\private.json`
+
+ 5. Open your new `private.json` file with your preferred text editor (such as TexEdit in macOS or Notepad in
+ Windows), then copy and paste the above cookies into their respective fields.
+ ***Please note, the `swid` will be surrounded by curly braces (`{...}`), which must be included.***
+
+**NOTE**: *Because ESPN made the change to their API between 2018 and 2019, ESPN support in the Fantasy Football Metrics
+Weekly Report application is currently limited to the 2019 season and later. Support for historical seasons will be
+implemented at a later time.
##### You are now ready to [generate a report!](#running-the-report-application)
---
+
##### CBS Setup
-CBS has a public API that was once documented, the last version of which can be viewed using the Wayback machine [here](https://web.archive.org/web/20150906150045/http://developer.cbssports.com/documentation). The Fantasy Football Metrics Weekly Report application uses this API to retrieve the necessary data to generate reports. ***Please note, some data required to provide certain information to the report is not currently available in the CBS API, so a few small things are excluded in the report until such a time as the data becomes available***. That being said, the missing data does not fundamentally limit the capability of the app to generate a complete report.
-``
+CBS has a public API that was once documented, the last version of which can be viewed using the Wayback
+machine [here](https://web.archive.org/web/20150906150045/http://developer.cbssports.com/documentation). The Fantasy
+Football Metrics Weekly Report application uses this API to retrieve the necessary data to generate reports.
+***Please note, some data required to provide certain information to the report is not currently available in the CBS
+API, so a few small things are excluded in the report until such a time as the data becomes available***. That being
+said, the missing data does not fundamentally limit the capability of the app to generate a complete report.
+
1. Retrieve your CBS league ID. You can find it by looking at the URL of your league in your browser:
![cbs-fantasy-football-league-id-location.png](resources/images/cbs-fantasy-football-league-id-location.png)
-2. Change the `league_id` value in `config.ini` to the above located league id.
+2. Change the `LEAGUE_ID` value in the `.env` file to the above located league id.
-3. The CBS API requires authentication to retrieve your league data, so you will need to use your CBS credentials to do so. Copy the file `private.template.json` (located in the `auth/cbs/` directory), and rename the file copy `private.json` by running the below command in your command line shell:
+3. The CBS API requires authentication to retrieve your league data, so you will need to use your CBS credentials to do
+ so. Copy the file `private.template.json` (located in the `auth/cbs/` directory), and rename the file
+ copy `private.json` by running the below command in your command line shell:
* **macOS**/**Linux**: `cp auth/cbs/private.template.json private.json auth/cbs/private.json`
* **Windows**: `copy auth\cbs\private.template.json auth\cbs\private.json`
-4. Open your new `private.json` file with your preferred text editor (such as TexEdit in macOS or Notepad in Windows), then update the respective fields with your `league_id`, CBS username, and CBS password. ***Please note, the FFMWR app does not store any of your credentials, it simply uses them to log in to your account and obtain an API access token.*** All values added to `private.json` must be surrounded by regular quotes (`""`), NOT formatted quotes (`“”`)).
+4. Open your new `private.json` file with your preferred text editor (such as TexEdit in macOS or Notepad in Windows), then update the respective fields with your `LEAGUE_ID`, CBS username, and CBS password. ***Please note, the FFMWR app does not store any of your credentials, it simply uses them to log in to your account and obtain an API access token.*** All values added to `private.json` must be surrounded by regular quotes (`""`), NOT formatted quotes (`“”`)).
-5. The first time you run the app, it will retrieve an API access token using your credentials and store it in the `auth/cbs/private.json` file. All subsequent runs of the report will simply use this access token to authenticate with the CBS API instead of your CBS credentials, so if you prefer you can delete your CBS username and CBS password from the file.
+5. The first time you run the app, it will retrieve an API access token using your credentials and store it in
+ the `auth/cbs/private.json` file. All subsequent runs of the report will simply use this access token to authenticate
+ with the CBS API instead of your CBS credentials, so if you prefer you can delete your CBS username and CBS password
+ from the file.
##### You are now ready to [generate a report!](#running-the-report-application)
---
+
### Running the Report Application
-1. Make sure you have updated the default league ID (`league_id` value) in the `config.ini` file to your own league id. Please see the respective setup instructions for your chosen platform for directions on how to find your league ID.
+1. Make sure you have updated the default league ID (`LEAGUE_ID` in the `.env` file) to your own league id. Please see
+ the respective setup instructions for your chosen platform for directions on how to find your league ID.
-2. From within the application directory (you should already be inside the `fantasy-football-metrics-weekly-report` directory) , run:
+2. From within the application directory (you should already be inside the `fantasy-football-metrics-weekly-report`
+ directory) , run:
```bash
docker compose up -d
```
- 1. *FIRST TIME RUNNING*: The first time you run the above command, you must wait for the Docker image to be pulled from the [GitHub Container Registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry). You will see output containing progress bars as the image layers are downloaded.
-
- **NOTE**: If you are running *Docker for Windows* and you see errors when trying to build the Docker container and/or run `docker compose up -d`, please go to the [Docker on Windows](#docker-on-windows) section in [Troubleshooting](#troubleshooting) for workarounds!
+ 1. *FIRST TIME RUNNING*: The first time you run the above command, you must wait for the Docker image to be pulled
+ from
+ the [GitHub Container Registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry).
+ You will see output containing progress bars as the image layers are downloaded.
+
+ **NOTE**: If you are running *Docker for Windows* and you see errors when trying to build the Docker container
+ and/or run `docker compose up -d`, please go to the [Docker on Windows](#docker-on-windows) section
+ in [Troubleshooting](#troubleshooting) for workarounds!
+
+ 2. *ALL RUNS*: After the initial run of the Docker container, you will not see all the image pull output as you did
+ the first time. However, for all runs you will know when the app is ready when you see the below output:
- 2. *ALL RUNS*: After the initial run of the Docker container, you will not see all the image pull output as you did the first time. However, for all runs you will know when the app is ready when you see the below output:
-
```bash
fantasy-football-metrics-weekly-report-app-1 | Fantasy Football Metrics Weekly Report app is ready!
```
-
+
3. The docker image is now running and ready for use!
-
+
3. Run the report:
```bash
docker exec -it fantasy-football-metrics-weekly-report-app-1 python main.py
```
- 1. You should see the following prompts:
-
- 1. `Generate report for default league? (y/n) -> `.
-
- Type `y` and hit enter.
-
- 2. `Generate report for default week? (y/n) ->`.
-
- Type `y` and hit enter.
-
- 3. **NOTE FOR YAHOO USERS ONLY**: The ***FIRST*** time you run the app, you will see an `AUTHORIZATION URL` (if you followed the instructions in the [Yahoo Setup](#yahoo-setup) section).
-
+ 1. You should see the following prompts:
+
+ 1. `Generate report for default league? (y/n) -> `.
+
+ Type `y` and hit enter.
+
+ 2. `Generate report for default week? (y/n) ->`.
+
+ Type `y` and hit enter.
+
+ 3. **NOTE FOR YAHOO USERS ONLY**: The ***FIRST*** time you run the app, you will see
+ an `AUTHORIZATION URL` (if you followed the instructions in the [Yahoo Setup](#yahoo-setup) section).
+
1. Click the link (or copy and paste it into your web browser).
-
- 2. The browser window should display a message asking for access to Yahoo Fantasy Sports on your account. Click `Accept`.
-
- 3. You should then see a verifier code (something like `w6nwjvz`). Copy the verifier code and return to the command line window, where you should now see the following prompt:
-
+
+ 2. The browser window should display a message asking for access to Yahoo Fantasy Sports on your account.
+ Click `Accept`.
+
+ 3. You should then see a verifier code (something like `w6nwjvz`). Copy the verifier code and return to the
+ command line window, where you should now see the following prompt:
+
```bash
Enter verifier :
```
Paste the verifier code there and hit enter.
-
- 4. Assuming the above went as expected, the application should now generate a report for your Yahoo fantasy league for the selected NFL week.
-
-4. When you are *done* using the report app, it is recommended that you *shut down* the Docker container in which it is running. You can do so by running:
+
+ 4. Assuming the above went as expected, the application should now generate a report for your Yahoo fantasy
+ league for the selected NFL week.
+
+4. When you are *done* using the report app, it is recommended that you *shut down* the Docker container in which it is
+ running. You can do so by running:
```bash
docker compose down
```
-
- The next time you run the app, you can simply re-run `docker compose up -d` to restart the container.
-
-***NOTE***: You can also specify a large number of configuration options directly in the command line. Please see the [usage section](#usage) for more information.
+
+The next time you run the app, you can simply re-run `docker compose up -d` to restart the container.
+
+***NOTE***: You can also specify a large number of settings directly in the command line. Please see
+the [usage section](#usage) for more information.
---
-
-### Configuration
+
-The Fantasy Football Metrics Weekly Report application allows certain aspects of the generated report to be configured with a `.ini` file. Included in the repository is `config.template.ini`, containing default values, as well as league settings that point to a public Yahoo league as a "demo" of the app.
+### Settings
-The app ***REQUIRES*** that `config.ini` be present (or that the user has provided an `.ini` configuration file), so it is recommended that you *make a copy* of the `config.template.ini` where it is already located, and then *rename the copy* to just `config.ini`. Then update the values to reflect the league for which you wish to generate a report, as well as any other settings you wish to change from the default values. ***You can also pass your own configuration `.ini` file to the app using the `-c` (`--config-file`) command line argument.***
+The Fantasy Football Metrics Weekly Report application allows certain aspects of the generated report to be set using
+a `.env` file, which defines a set of environment variables the app reads when it runs. The first time you run the FFMWR
+app *without* a `.env` file present, it will ask you if you would like to generate one and prompt you for several
+initial values for the file. It will then create a `.env` file in the root directory of the app which will be populated
+by a selection of default values (all of which can be changed as needed).
+
+The app ***REQUIRES*** that a `.env` be present, so it is recommended that you allow the app to generate a
+default `.env` file for you and then update the values to reflect the league for which you wish to generate a report, as
+well as modify any other settings you wish to change from the default values.
+
#### Report Features
-For those of you who wish to configure the report to include a custom subset of the available features (for instance, if you want league stats but not team pages, or if you want score rankings but not coaching efficiency), the `Report` section in the config file allows all features to be turned on or off. You must use a boolean value (`True` or `False`) to turn on/off any of the available report features, which are the following:
-
- league_standings = True
- league_playoff_probs = True
- league_median_standings = True
- league_power_rankings = True
- league_z_score_rankings = True
- league_score_rankings = True
- league_coaching_efficiency_rankings = True
- league_luck_rankings = True
- league_optimal_score_rankings = True
- league_bad_boy_rankings = True
- league_beef_rankings = True
- league_covid_risk_rankings = True
- league_weekly_top_scorers = True
- league_weekly_highest_ce = True
- report_time_series_charts = True
- report_team_stats = True
- team_points_by_position_charts = True
- team_bad_boy_stats = True
- team_beef_stats = True
- team_boom_or_bust = True
+For those of you who wish for the report to include a custom subset of the available features (for instance, if you want
+league stats but not team pages, or if you want score rankings but not coaching efficiency), the `REPORT SETTINGS`
+section in the `.env` file allows all features to be turned on or off. You must use a boolean value (`True` or `False`)
+to turn on/off any of the available report features, which are the following:
+
+ LEAGUE_STANDINGS_BOOL=True
+ LEAGUE_PLAYOFF_PROBS_BOOL=True
+ LEAGUE_MEDIAN_STANDINGS_BOOL=True
+ LEAGUE_POWER_RANKINGS_BOOL=True
+ LEAGUE_Z_SCORE_RANKINGS_BOOL=True
+ LEAGUE_SCORE_RANKINGS_BOOL=True
+ LEAGUE_COACHING_EFFICIENCY_RANKINGS_BOOL=True
+ LEAGUE_LUCK_RANKINGS_BOOL=True
+ LEAGUE_OPTIMAL_SCORE_RANKINGS_BOOL=True
+ LEAGUE_BAD_BOY_RANKINGS_BOOL=True
+ LEAGUE_BEEF_RANKINGS_BOOL=True
+ LEAGUE_WEEKLY_TOP_SCORERS_BOOL=True
+ LEAGUE_WEEKLY_HIGHEST_CE_BOOL=True
+ REPORT_TIME_SERIES_CHARTS_BOOL=True
+ REPORT_TEAM_STATS_BOOL=True
+ TEAM_POINTS_BY_POSITION_CHARTS_BOOL=True
+ TEAM_BAD_BOY_STATS_BOOL=True
+ TEAM_BEEF_STATS_BOOL=True
+ TEAM_BOOM_OR_BUST_BOOL=True
+
#### Report Formatting
-The report can also have some of its visual formatting configured. The following formatting options are available:
+The report can also have some of its visual formatting set. The following formatting options are available:
+
+ FONT=helvetica
+ SUPPORTED_FONTS=helvetica,times,symbola,opensansemoji,sketchcollege,leaguegothic
+ FONT_SIZE=12
+ IMAGE_QUALITY=75
+ MAX_DATA_CHARS=24
- font = helvetica
- font_size = 12
-
-The values seen in the `supported_fonts` configuration option are the currently supported fonts for the app.
+The values seen in the `SUPPORTED_FONTS` environment variable are the currently supported fonts for the app.
-The player headshots retrieved for individual team pages can come in varying resolutions, and when they are extremely high resolution, they can inflate the size of the report PDF. In order to allow the user to reduce the size of the final report PDF if desired, the following option is available:
+The player headshots retrieved for individual team pages can come in varying resolutions, and when they are extremely
+high resolution, they can inflate the size of the report PDF. In order to allow the user to reduce the size of the final
+report PDF if desired, the following option is available:
- image_quality = 75
-
-The default value for the image quality is 75%, allowing for a reasonable reduction in image size without sacrificing overall aesthetic quality. However, this value can be set on a scale of 0%-100%, depending on the preferences of the user.
+ IMAGE_QUALITY=75
-Once the initial images have been retrieved and quality has been adjusted, the report will cache those images and continue to use those indefinitely until you delete the `output/data///week_/player_headshots` for that week, since otherwise the images would continue to have their quality reduced until the headshots degraded entirely.
+The default value for the image quality is 75%, allowing for a reasonable reduction in image size without sacrificing
+overall aesthetic quality. However, this value can be set on a scale of 0%-100%, depending on the preferences of the
+user.
+
+Once the initial images have been retrieved and quality has been adjusted, the report will cache those images and
+continue to use those indefinitely until you delete the `output/data///week_/player_headshots`
+for that week, since otherwise the images would continue to have their quality reduced until the headshots degraded
+entirely.
+
#### Report Settings
-In addition to turning on/off the features of the report PDF itself, there are additional configuration options, which are as follows:
-
-| Option | Description |
-|-----------------------------------------:|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `platform` | Fantasy football platform for which you are generating a report. |
-| `supported_platforms` | Comma-delimited list of currently supported fantasy football platforms. |
-| `league_id` | The league id of the fantasy football for which you are running the report. |
-| `game_id` | Game id by season (see: [Game Resource](https://developer.yahoo.com/fantasysports/guide/game-resource.html#game-resource-desc) for Yahoo) |
-| `data_dir` | Directory where saved data is stored. |
-| `output_dir` | Directory where generated reports are created. |
-| `chosen_week` | Selected NFL season week for which to generate a report. |
-| `num_playoff_simulations` | Number of Monte Carlo simulations to run for playoff predictions. The more sims, the longer the report will take to generate. |
-| `bench_positions` | Comma-delimited list of available bench positions in your league. |
-| `prohibited_statuses` | Comma-delimited list of possible statuses in your league that indicate a player was not able to play (only needed if you plan to utilize the automated coaching efficiency disqualification functionality). |
-| `initial_faab_budget` | Set the initial FAAB (Free Agent Acquisition Budget) for Yahoo leagues, since this information does not seem to be exposed in the API. |
-| `num_teams` | Number of teams in selected league. |
-| `num_regular_season_weeks` | Number of regular season weeks in selected league. |
-| `num_playoff_slots` | Number of playoff slots in selected league. |
-| `num_playoff_slots_per_division` | Numbers of teams per division that qualify for the playoffs. |
-| `coaching_efficiency_disqualified_teams` | Teams manually DQed from coaching efficiency rankings (if any). |
-| `yahoo_auth_dir` | Directory where Yahoo OAuth accesses and stores credentials and refresh tokens. |
-| `google_drive_upload` | Turn on (`True`) or off (`False`) the Google Drive upload functionality. |
-| `google_drive_auth_token` | Google OAuth refresh token. |
-| `google_drive_root_folder_name` | Online folder in Google Drive where reports are uploaded. |
-| `google_drive_reupload_file` | File path of selected report that you wish to re-upload to Google Drive by running `upload_to_google_drive.py` as a standalone script. |
-| `post_to_slack` | Turn on (`True`) or off (`False`) the Slack upload functionality. |
-| `slack_auth_token` | Slack authentication token. |
-| `post_or_file` | Choose whether you post a link to the generated report on Slack (set to `post`), or upload the report PDF itself to Slack (set to `file`). |
-| `slack_channel` | Selected Slack channel where reports are uploaded. |
-| `notify_channel` | Turn on (`True`) or off (`False`) using the `@here` slack tag to notify chosen Slack channel of a posted report file. |
-| `repost_file` | File path of selected report that you wish to repost to Slack. |
+In addition to turning on/off the features of the report PDF itself, there are additional setting, which are as follows:
+
+| Option | Description |
+|-----------------------------------------:|:-------------------------------------------------------------------------------------------------------------------------------------------|
+| `PLATFORM` | Fantasy football platform for which you are generating a report. |
+| `SUPPORTED_PLATFORMS` | Comma-delimited list of currently supported fantasy football platforms. |
+| `LEAGUE_ID` | The league id of the fantasy football for which you are running the report. |
+| `DATA_DIR_LOCAL_PATH` | Directory where saved data is stored. |
+| `OUTPUT_DIR_LOCAL_PATH` | Directory where generated reports are created. |
+| `WEEK_FOR_REPORT` | Selected NFL season week for which to generate a report. |
+| `NUM_PLAYOFF_SIMULATIONS` | Number of Monte Carlo simulations to run for playoff predictions. The more sims, the longer the report will take to generate. |
+| `NUM_REGULAR_SEASON_WEEKS` | Number of regular season weeks in selected league. |
+| `NUM_PLAYOFF_SLOTS` | Number of playoff slots in selected league. |
+| `NUM_PLAYOFF_SLOTS_PER_DIVISION` | Numbers of teams per division that qualify for the playoffs. |
+| `COACHING_EFFICIENCY_DISQUALIFIED_TEAMS` | Teams manually DQed from coaching efficiency rankings (if any). |
+| `GOOGLE_DRIVE_UPLOAD_BOOL` | Turn on (`True`) or off (`False`) the Google Drive upload functionality. |
+| `GOOGLE_DRIVE_AUTH_TOKEN_LOCAL_PATH` | Google OAuth refresh token. |
+| `GOOGLE_DRIVE_FOLDER_PATH` | Online folder in Google Drive where reports are uploaded. |
+| `GOOGLE_DRIVE_REUPLOAD_FILE_LOCAL_PATH` | File path of selected report that you wish to re-upload to Google Drive by running `upload_to_google_drive.py` as a standalone script. |
+| `SLACK_POST_BOOL` | Turn on (`True`) or off (`False`) the Slack upload functionality. |
+| `SLACK_AUTH_TOKEN_LOCAL_PATH` | Slack authentication token. |
+| `SLACK_POST_OR_FILE` | Choose whether you post a link to the generated report on Slack (set to `post`), or upload the report PDF itself to Slack (set to `file`). |
+| `SLACK_CHANNEL` | Selected Slack channel where reports are uploaded. |
+| `SLACK_CHANNEL_NOTIFY_BOOL` | Turn on (`True`) or off (`False`) using the `@here` slack tag to notify chosen Slack channel of a posted report file. |
+| `SLACK_REPOST_FILE_LOCAL_PATH` | File path of selected report that you wish to repost to Slack. |
---
+
### Usage
-After completing the above setup and configuration steps, you should now be able to simply run `docker exec -it fantasy-football-metrics-weekly-report_app_1 python main.py` to regenerate a report. The report generator script (`main.py`) also supports several command line options/arguments that allow you to specify the following:
-
-| Flag | Description |
-|:-------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------|
-| `-h`, `--help` | Display command line usage message |
-| `-d`, `--use-default` | Automatically run the report using the default configuration without user input prompts. |
-| `-f`, `--fantasy-platform` `` | Fantasy football platform on which league for report is hosted. |
-| `-l`, `--league-id` `` | Fantasy Football league ID |
-| `-w`, `--week` `` | Chosen week for which to generate report |
-| `-g`, `--game-id` `` | Chosen fantasy game id for which to generate report. Defaults to "nfl", interpreted as the current season if using Yahoo. |
-| `-y`, `--year` `` | Chosen year (season) of the league for which a report is being generated. |
-| `-c`, `--config-file` `` | System file path (including file name) for .ini file to be used for configuration. |
-| `-s`, `--save-data` | Save all retrieved data locally for faster future report generation |
-| `-s`, `--refresh-web-data` | Refresh all web data from external APIs (such as bad boy and beef data) |
-| `-p`, `--playoff-prob-sims` `` | Number of Monte Carlo playoff probability simulations to run." |
-| `-b`, `--break-ties` | Break ties in metric rankings |
-| `-q`, `--disqualify-ce` | Automatically disqualify teams ineligible for coaching efficiency metric |
-| `-o`, `--offline` | Run ***OFFLINE*** (for development). Must have previously run report with -s option. |
-| `-t`, `--test` | Generate TEST report (for development) |
-
-#### NOTE: all command line arguments ***OVERRIDE*** any settings configured in the local config.ini file!
+After completing the above setup and settings steps, you should now be able to simply
+run `docker exec -it fantasy-football-metrics-weekly-report_app_1 python main.py` to regenerate a report. The report
+generator script (`main.py`) also supports several command line options/arguments that allow you to specify the
+following:
+
+| Flag | Description |
+|:----------------------------------------|:--------------------------------------------------------------------------------------------------------------------------|
+| `-h`, `--help` | Display command line usage message |
+| `-d`, `--use-default` | Automatically run the report using the default settings without user input prompts. |
+| `-f`, `--fantasy-platform` `` | Fantasy football platform on which league for report is hosted. |
+| `-l`, `--league-id` `` | Fantasy Football league ID |
+| `-w`, `--week` `` | Chosen week for which to generate report |
+| `-g`, `--game-id` `` | Chosen fantasy game id for which to generate report. Defaults to "nfl", interpreted as the current season if using Yahoo. |
+| `-y`, `--year` `` | Chosen year (season) of the league for which a report is being generated. |
+| `-s`, `--save-data` | Save all retrieved data locally for faster future report generation |
+| `-s`, `--refresh-web-data` | Refresh all web data from external APIs (such as bad boy and beef data) |
+| `-p`, `--playoff-prob-sims` `` | Number of Monte Carlo playoff probability simulations to run." |
+| `-b`, `--break-ties` | Break ties in metric rankings |
+| `-q`, `--disqualify-ce` | Automatically disqualify teams ineligible for coaching efficiency metric |
+| `-o`, `--offline` | Run ***OFFLINE*** (for development). Must have previously run report with -s option. |
+| `-t`, `--test` | Generate TEST report (for development) |
+
+#### NOTE: all command line arguments ***OVERRIDE*** any settings in the local .env file!
##### Example:
```bash
docker exec -it fantasy-football-metrics-weekly-report_app_1 python main.py -l 140941 -f fleaflicker -y 2020 -w 3 -p 1000 -s -r
```
-
-The above command runs the report with the following configuration options (which override anything set in `config.ini`):
-* Platform: `fleaflicker`
+The above command runs the report with the following settings (which override anything set in the `.env` file):
+
+* Platform: `fleaflicker`
* League id: `140941`
-
+
* NFL season: `2020`
* NFL week: `3`
@@ -632,25 +782,31 @@ The above command runs the report with the following configuration options (whic
* Saves the data locally (`-s`)
-* Refreshes any previously saved local data (`-r`)
+* Refreshes any previously saved local data (`-r`)
---
+
### Additional Integrations
-The Fantasy Football Metrics Weekly Report application also supports several additional integrations if you choose to utilize them. Currently, it is capable of uploading your generated reports to Google Drive, and also directly posting your generated reports to the Slack Messenger app.
+The Fantasy Football Metrics Weekly Report application also supports several additional integrations if you choose to
+utilize them. Currently, it is capable of uploading your generated reports to Google Drive, and also directly posting
+your generated reports to the Slack Messenger app.
+
#### Google Drive Setup
-The Fantasy Football Metrics Weekly Report application includes Google Drive integration, allowing your generated reports to be uploaded and stored in Google Drive, making it easy to share the report with all league members.
+The Fantasy Football Metrics Weekly Report application includes Google Drive integration, allowing your generated
+reports to be uploaded and stored in Google Drive, making it easy to share the report with all league members.
The following setup steps are ***required*** in order to allow the Google Drive integration to function properly:
1. Log in to your Google account (or make one if you don't have one).
-2. Create a [new project](https://console.developers.google.com/projectcreate?folder=&organizationId=0) in the Google Developers Console.
+2. Create a [new project](https://console.developers.google.com/projectcreate?folder=&organizationId=0) in the Google
+ Developers Console.
3. Accept the terms & conditions.
@@ -662,9 +818,12 @@ The following setup steps are ***required*** in order to allow the Google Drive
7. Go to the [Google Developers Console](https://console.developers.google.com/apis/dashboard).
-8. Your new project should automatically load in the dashboard, but in the event it does not, or you have other projects (a different project might load by default), click the project name on the top left of the page (to the right of where it says "Google APIs"), and select your new project.
+8. Your new project should automatically load in the dashboard, but in the event it does not, or you have other
+ projects (a different project might load by default), click the project name on the top left of the page (to the
+ right of where it says "Google APIs"), and select your new project.
-9. Either click the `+ ENABLE APIS AND SERVICES` button on the top of the page, or select "Library" from the menu on the left, search for `Google Drive API`, and click `Google Drive API` when it comes up.
+9. Either click the `+ ENABLE APIS AND SERVICES` button on the top of the page, or select "Library" from the menu on the
+ left, search for `Google Drive API`, and click `Google Drive API` when it comes up.
10. Click `ENABLE`.
@@ -684,7 +843,8 @@ The following setup steps are ***required*** in order to allow the Google Drive
18. Click `SAVE AND CONTINUE`.
-19. Click `ADD OR REMOVE SCOPES`, search for `google drive` in the filter, check the box next to the `../auth/drive` scope, and click `UPDATE` at the bottom.
+19. Click `ADD OR REMOVE SCOPES`, search for `google drive` in the filter, check the box next to the `../auth/drive`
+ scope, and click `UPDATE` at the bottom.
20. Click `SAVE AND CONTINUE`.
@@ -696,9 +856,11 @@ The following setup steps are ***required*** in order to allow the Google Drive
24. A popup with your `client ID` and `client secret` will appear. Click `OK`.
-25. On the far right of your new credential, click the little arrow that displays `Download OAuth Client` when you hover over it, then click `DOWNLOAD JSON`.
+25. On the far right of your new credential, click the little arrow that displays `Download OAuth Client` when you hover
+ over it, then click `DOWNLOAD JSON`.
-26. Your credentials JSON file will download. Rename it `credentials.json`, and put it in the `auth/google/` directory where `credentials.template.json` is located.
+26. Your credentials JSON file will download. Rename it `credentials.json`, and put it in the `auth/google/` directory
+ where `credentials.template.json` is located.
27. Open a terminal window (makes sure you are inside the `fantasy-football-metrics-weekly-report` directory), and run:
@@ -706,29 +868,50 @@ The following setup steps are ***required*** in order to allow the Google Drive
docker exec -it fantasy-football-metrics-weekly-report_app_1 python resources/google_quickstart.py --noauth_local_webserver
```
-28. You will see a message that says `Go to the following link in your browser:`, followed by a link. Copy the URL and paste it into a web browser, and hit enter. The open window will ask you to either select a Google account to log into (if you have multiple) or log in. Select your account/login.
+28. You will see a message that says `Go to the following link in your browser:`, followed by a link. Copy the URL and
+ paste it into a web browser, and hit enter. The open window will ask you to either select a Google account to log
+ into (if you have multiple) or log in. Select your account/login.
-29. A warning screen will appear saying "This app isn't verified". Click "Advanced" and then "Go to yff-report-drive-uploader (unsafe)" (this screen may vary depending on your web browser, but the point is you need to proceed past the warning).
+29. A warning screen will appear saying "This app isn't verified". Click "Advanced" and then "Go to
+ yff-report-drive-uploader (unsafe)" (this screen may vary depending on your web browser, but the point is you need
+ to proceed past the warning).
-30. On the next screen, a popup saying "Grant yff-report-drive-uploader permission" will appear. Click "Allow", then "Allow" again on the following "Confirm your choices" screen.
+30. On the next screen, a popup saying "Grant yff-report-drive-uploader permission" will appear. Click "Allow", then "
+ Allow" again on the following "Confirm your choices" screen.
-31. Next you will see a screen that says only "Please copy this code, switch to your application and paste it there:". Copy the code, and return to your open terminal window (you can close the browser window once you've copied the verification code).
+31. Next you will see a screen that says only "Please copy this code, switch to your application and paste it there:".
+ Copy the code, and return to your open terminal window (you can close the browser window once you've copied the
+ verification code).
32. Paste the verification code where it says `Enter verification code:`, and hit enter.
-33. You should then see the command line output "Authentication successful.", as well as a list of 10 files in your Google Drive to confirm it can access your drive. It will also have automatically generated a `token.json` file in `auth/google/`, which you should just leave where it is and do ***NOT*** edit or modify in any way!
-
-34. You can now upload your reports to Google Drive in one of two ways listed below. ***Please note, if you wish to specify where the app will upload the report to in Google Drive, change the value of `google_drive_folder_path` in `config.ini` to whatever path you wish to store the reports in Google Drive, such as `Fantasy_Football/reports`. If you do not put a path in this value the report will default to uploading files to a `Fantasy_Football` directory at the root of your Google Drive.***
- 1. Change `google_drive_upload` to `True` in `config.ini` and generate a new report. You will see a message at the end of the run that indicates the report PDF was successfully uploaded to Google Drive, and provides the direct share link to the file.
+33. You should then see the command line output "Authentication successful.", as well as a list of 10 files in your
+ Google Drive to confirm it can access your drive. It will also have automatically generated a `token.json` file
+ in `auth/google/`, which you should just leave where it is and do ***NOT*** edit or modify in any way!
+
+34. You can now upload your reports to Google Drive in one of two ways listed below.
+ ***Please note, if you wish to specify where the app will upload the report to in Google Drive, change the value
+ of `GOOGLE_DRIVE_FOLDER_PATH` in the `.env` file to whatever path you wish to store the reports in Google Drive,
+ such as `Fantasy_Football/reports`. If you do not put a path in this value the report will default to uploading
+ files to a `Fantasy_Football` directory at the root of your Google Drive.***
+ 1. Change `GOOGLE_DRIVE_UPLOAD_BOOL` to `True` in the `.env` file and generate a new report. You will see a message
+ at the end of the run that indicates the report PDF was successfully uploaded to Google Drive, and provides the
+ direct share link to the file.
**OR**
- 2. Set the value of `google_drive_reupload_file` in `config.ini` to the local filepath of the report you wish to upload, opening a Terminal window, and running `python integrations/drive.py`*. This only works for preexisting reports that you have already generated, and will then upload that report to Google Drive without generating a new one.
+ 2. Set the value of `GOOGLE_DRIVE_REUPLOAD_FILE_LOCAL_PATH` in the `.env` file to the local filepath of the report
+ you wish to upload, opening a Terminal window, and running `python integrations/drive.py`*. This only works for
+ preexisting reports that you have already generated, and will then upload that report to Google Drive without
+ generating a new one.
---
+
#### Slack Setup
-The Fantasy Football Metrics Weekly Report application includes integration with the popular personal and business chat app Slack, allowing your generated reports (or links to where they are stored on Google Drive) to be uploaded directly to Slack, making it easy to share the report with all league members.
+The Fantasy Football Metrics Weekly Report application includes integration with the popular personal and business chat
+app Slack, allowing your generated reports (or links to where they are stored on Google Drive) to be uploaded directly
+to Slack, making it easy to share the report with all league members.
The following setup steps are ***required*** in order to allow the Slack integration to function properly:
@@ -739,10 +922,12 @@ The following setup steps are ***required*** in order to allow the Slack integra
3. After the popup appears, fill in the fields as follows:
* i. `App Name`: `ff-report` (this name can be anything you want)
-
+
* ii. `Development Slack Workspace`: Select your chosen Slack workspace from the dropdown menu.
-
-4. Click `Create App`. You should now be taken to the page for your new app, where you can configure things like the app title card color, the icon, the description, as well as a whole host of other features (see [here](https://api.slack.com/slack-apps) for more information).
+
+4. Click `Create App`. You should now be taken to the page for your new app, where you can set things like the app title
+ card color, the icon, the description, as well as a whole host of other features (
+ see [here](https://api.slack.com/slack-apps) for more information).
5. Select `Basic Information` from the menu on the left.
@@ -761,9 +946,9 @@ The following setup steps are ***required*** in order to allow the Slack integra
12. Under `Bot Token Scopes`, click the `Add an OAuth Scope` button.
13. From the dropdown menu, select the below scopes:
-
+
| OAuth Scope | Description |
- |:-----------------------|:------------------------------------------------------------------------------------|
+ |:-----------------------|:------------------------------------------------------------------------------------|
| `channels:read` | View basic information about public channels in the workspace |
| `chat:write` | Send messages as @ff-report |
| `chat:write.customize` | Send messages as @ff-report with a customized username and avatar |
@@ -774,38 +959,56 @@ The following setup steps are ***required*** in order to allow the Slack integra
| `incoming-webhook` | Post messages to specific channels in Slack |
| `mpim:read` | View basic information about group direct messages that ff-report has been added to |
-14. Scroll back up to `OAuth Tokens & Redirect URLs`, and now you should be able to click the `Install App to Workspace` button, so click it.
+14. Scroll back up to `OAuth Tokens & Redirect URLs`, and now you should be able to click the `Install App to Workspace`
+ button, so click it.
+
+15. You will be redirected to a screen saying your app is asking for permission to access the Slack workspace, and
+ presenting you with a dropdown to select a channel for your app to post to. Select your desired channel, and
+ hit `Allow`.
+
+16. You will now be redirected back to the `OAuth & Permissions` section of your app settings. At the top, you will see
+ a `Bot User OAuth Access Token` field, which will now have a value populated.
-15. You will be redirected to a screen saying your app is asking for permission to access the Slack workspace, and presenting you with a dropdown to select a channel for your app to post to. Select your desired channel, and hit `Allow`.
+17. Copy the file `token.template.json` (located in the `auth/slack/` directory), and rename the file copy `token.json`,
+ then copy and paste the above `Bot user OAuth Access Token` into the field value of `token.json` where it
+ says `"SLACK_APP_OAUTH_ACCESS_TOKEN_STRING"`, replacing that string. Make sure you are using double quotes (`"`) on
+ either side of your token string.
-16. You will now be redirected back to the `OAuth & Permissions` section of your app settings. At the top, you will see a `Bot User OAuth Access Token` field, which will now have a value populated.
+18. If you are posting to a *private channel*, you will need to invite the bot to the channel before it can make posts
+ there. Just go to the Slack channel and type `@ff-report`, and then hit enter. Slack will ask if you wish to invite
+ the bot to the channel, so confirm that you wish to add the bot to the channel, and now it should be able to post to
+ the *private channel*.
-17. Copy the file `token.template.json` (located in the `auth/slack/` directory), and rename the file copy `token.json`, then copy and paste the above `Bot user OAuth Access Token` into the field value of `token.json` where it says `"SLACK_APP_OAUTH_ACCESS_TOKEN_STRING"`, replacing that string. Make sure you are using double quotes (`"`) on either side of your token string.
+19. *You can now upload your reports to Slack, either by updating the following values in the `.env` file:*
-18. If you are posting to a *private channel*, you will need to invite the bot to the channel before it can make posts there. Just go to the Slack channel and type `@ff-report`, and then hit enter. Slack will ask if you wish to invite the bot to the channel, so confirm that you wish to add the bot to the channel, and now it should be able to post to the *private channel*.
+ * i. `SLACK_POST_BOOL=True`
-19. *You can now upload your reports to Slack, either by updating the following values in `config.ini`:*
+ * ii. `SLACK_CHANNEL=channel-name` (this can be set to whichever channel you wish to post (as long as the user who
+ created the app has access to that channel)
- * i. `post_to_slack = True`
-
- * ii. `slack_channel = channel-name` (this can be set to whichever channel you wish to post (as long as the user who created the app has access to that channel)
-
- *Or by setting the value of `repost_file` in `config.ini` to the filepath of the report you wish to upload, opening a Terminal window, and running `python integrations/slack.py`*.
+ *Or by setting the value of `SLACK_REPOST_FILE_LOCAL_PATH` in the `.env` file to the filepath of the report you wish
+ to upload, opening a Terminal window, and running `python integrations/slack.py`*.
---
+
### Troubleshooting
+
#### Logs
-In addition to displaying output from the application to the command line, the Fantasy Football Metrics Weekly Report also logs all the same output to [out.log](logs/out.log), which you can view at any time to see output from past runs of the application.
+In addition to displaying output from the application to the command line, the Fantasy Football Metrics Weekly Report
+also logs all the same output to [out.log](logs/out.log), which you can view at any time to see output from past runs of
+the application.
+
#### Yahoo
-Occasionally when you use the Yahoo fantasy football API, there are hangups on the other end that can cause data not to transmit, and you might encounter an error similar to this:
+Occasionally when you use the Yahoo fantasy football API, there are hangups on the other end that can cause data not to
+transmit, and you might encounter an error similar to this:
Traceback (most recent call last):
File "yfpy-app.py", line 114, in
@@ -814,41 +1017,60 @@ Occasionally when you use the Yahoo fantasy football API, there are hangups on t
for team in team_standings:
IndexError: list index out of range
-Typically, when the above error (or a similar error) occurs, it simply means that one of the Yahoo Fantasy Football API calls failed and so no data was retrieved. This can be fixed by simply re-running data query.
+Typically, when the above error (or a similar error) occurs, it simply means that one of the Yahoo Fantasy Football API
+calls failed and so no data was retrieved. This can be fixed by simply re-running data query.
+
#### Docker on Windows
-If you are running Docker on Windows, you might encounter errors when trying to build the Docker image and/or run `docker compose up -d`. Typically, these errors revolve around the way Windows strictly enforces file access permissions. There are two known permissions issues (and workarounds) currently for running the FFMWR app.
+If you are running Docker on Windows, you might encounter errors when trying to build the Docker image and/or
+run `docker compose up -d`. Typically, these errors revolve around the way Windows strictly enforces file access
+permissions. There are two known permissions issues (and workarounds) currently for running the FFMWR app.
-1. If you are running on ***Windows 10 Enterprise, Pro, or Education*** (all of which support the Hyper-V feature), then the latest version of Docker for Windows requires you to specifically give Docker permission to access any files and directories you need it to be able to see.
+1. If you are running on ***Windows 10 Enterprise, Pro, or Education*** (all of which support the Hyper-V
+ feature), then the latest version of Docker for Windows requires you to specifically give Docker permission to access
+ any files and directories you need it to be able to see.
1. In order to do so, open up Docker for Windows, and go to settings:
- ![docker-settings.png](resources/images/docker-settings.png)
+ ![docker-settings.png](resources/images/docker-settings.png)
2. Then click the following items in order (stop between 3 and 4):
-
- ![docker-file-sharing.png](resources/images/docker-file-sharing.png)
- 3. After clicking the `+` button to add a directory, select the FFMWR app directory (which will be wherever you cloned it), or any parent directory of the app directory, and add it. Then click `Apply & Restart`.
-
- 4. Now go back to your command line shell, make sure you are in the FFMWR app directory, and re-run `docker compose up -d`. This time things should build and startup as expected without any errors, and you can pick up where you left of with [Running the Report Application](#running-the-report-application)!
-
-2. If you are running on ***Windows 10 Home*** (which does **not** support the Hyper-V feature), then Docker for Windows does not have the File Sharing option discussed above for Windows 10 Enterprise, Pro, and Education users. However, you might still run into similar permissions issues. The below steps should provide a workaround to just sharing the files in Docker Desktop for Windows:
+ ![docker-file-sharing.png](resources/images/docker-file-sharing.png)
+
+ 3. After clicking the `+` button to add a directory, select the FFMWR app directory (which will be wherever you
+ cloned it), or any parent directory of the app directory, and add it. Then click `Apply & Restart`.
+
+ 4. Now go back to your command line shell, make sure you are in the FFMWR app directory, and
+ re-run `docker compose up -d`. This time things should build and startup as expected without any errors, and you
+ can pick up where you left of with [Running the Report Application](#running-the-report-application)!
+
+2. If you are running on ***Windows 10 Home*** (which does **not** support the Hyper-V feature), then Docker
+ for Windows does not have the File Sharing option discussed above for Windows 10 Enterprise, Pro, and Education
+ users. However, you might still run into similar permissions issues. The below steps should provide a workaround to
+ just sharing the files in Docker Desktop for Windows:
1. Type `Windows + X`. You will see a small pop-up list containing various administrator tasks.
-
+
2. Select `Command Prompt (Admin)`.
-
- 3. Use `cd` commands to navigate to whichever directory you cloned the FFMWR app into (e.g. `cd ~\Documents\fantasy-football-metrics-weekly-report\`).
-
- 4. Now from within that command prompt shell (which has privileged admin access), you should be able to re-run `docker compose up -d`, wait for everything to build and start running, and then pick up where you left of with [Running the Report Application](#running-the-report-application). Remember to stay in the admin command prompt shell to give your command the right file access!
+
+ 3. Use `cd` commands to navigate to whichever directory you cloned the FFMWR app into (
+ e.g. `cd ~\Documents\fantasy-football-metrics-weekly-report\`).
+
+ 4. Now from within that command prompt shell (which has privileged admin access), you should be able to
+ re-run `docker compose up -d`, wait for everything to build and start running, and then pick up where you left of
+ with [Running the Report Application](#running-the-report-application). Remember to stay in the admin command
+ prompt shell to give your command the right file access!
+
#### Reportlab
-On macOS 12+ (Monterey) or on Macs using the M1/M2 ARM architecture, you might encounter the following error (or one like it) during dependency installation:
+On macOS 12+ (Monterey) or on Macs using the M1/M2 ARM architecture, you might encounter the following error (or one
+like it) during dependency installation:
+
```shell
Symbol not found in flat namespace '_FT_Done_Face' from reportlab with Python@3.9 on macOS 12
```
diff --git a/calculate/bad_boy_stats.py b/calculate/bad_boy_stats.py
index fefa5fa6..6a913645 100644
--- a/calculate/bad_boy_stats.py
+++ b/calculate/bad_boy_stats.py
@@ -13,8 +13,8 @@
import requests
from bs4 import BeautifulSoup
-from report.constants import nfl_team_abbreviations, nfl_team_abbreviation_conversions
-from report.logger import get_logger
+from utilities.constants import nfl_team_abbreviations, nfl_team_abbreviation_conversions
+from utilities.logger import get_logger
logger = get_logger(__name__, propagate=False)
diff --git a/calculate/beef_stats.py b/calculate/beef_stats.py
index 98fd45a3..58f22c53 100644
--- a/calculate/beef_stats.py
+++ b/calculate/beef_stats.py
@@ -8,8 +8,8 @@
import requests
-from report.constants import nfl_team_abbreviations, nfl_team_abbreviation_conversions
-from report.logger import get_logger
+from utilities.constants import nfl_team_abbreviations, nfl_team_abbreviation_conversions
+from utilities.logger import get_logger
logger = get_logger(__name__, propagate=False)
diff --git a/calculate/coaching_efficiency.py b/calculate/coaching_efficiency.py
index 4eda7482..3875d23d 100644
--- a/calculate/coaching_efficiency.py
+++ b/calculate/coaching_efficiency.py
@@ -7,7 +7,8 @@
from typing import List, Dict, Set, Union
from dao.base import BasePlayer, BaseLeague
-from report.logger import get_logger
+from utilities.constants import prohibited_statuses
+from utilities.logger import get_logger
logger = get_logger(__name__, propagate=False)
@@ -49,13 +50,11 @@ def is_full(self):
class CoachingEfficiency(object):
- def __init__(self, config, league):
+ def __init__(self, league):
logger.debug("Initializing coaching efficiency.")
- self.config = config
-
self.inactive_statuses: List[str] = [
- str(status) for status in self.config.get("Configuration", "prohibited_statuses").split(",")
+ status for status in prohibited_statuses
]
self.league: BaseLeague = league
diff --git a/calculate/covid_risk.py b/calculate/covid_risk.py
deleted file mode 100644
index 3263bde5..00000000
--- a/calculate/covid_risk.py
+++ /dev/null
@@ -1,298 +0,0 @@
-__author__ = "Wren J. R. (uberfastman)"
-__email__ = "uberfastman@uberfastman.dev"
-
-import itertools
-import json
-from collections import OrderedDict
-from datetime import datetime, timedelta
-from pathlib import Path
-from typing import Dict
-
-import requests
-from bs4 import BeautifulSoup
-
-from report.constants import nfl_team_abbreviations, nfl_team_abbreviation_conversions
-from report.logger import get_logger
-from utilities.config import AppConfigParser
-
-logger = get_logger(__name__, propagate=False)
-
-
-class CovidRisk(object):
-
- def __init__(self, config, data_dir, season, week, save_data=False, offline=False, refresh=False):
- logger.debug("Initializing COVID-19 risk.")
-
- self.config: AppConfigParser = config
-
- self.season = int(season)
- self.week = int(week)
- self.selected_nfl_season_week = datetime.strptime(
- f"{self.season} {self.week + 36} 1",
- "%G %V %u"
- )
-
- self.save_data = save_data
- self.offline = offline
- self.refresh = refresh
-
- nfl_team_abbreviations_ref: Dict[str, str] = {
- "Arizona Cardinals": "ARI",
- "Atlanta Falcons": "ATL",
- "Baltimore Ravens": "BAL",
- "Buffalo Bills": "BUF",
- "Carolina Panthers": "CAR",
- "Chicago Bears": "CHI",
- "Cincinnati Bengals": "CIN",
- "Cleveland Browns": "CLE",
- "Dallas Cowboys": "DAL",
- "Denver Broncos": "DEN",
- "Detroit Lions": "DET",
- "Green Bay Packers": "GB",
- "Houston Texans": "HOU",
- "Indianapolis Colts": "IND",
- "Jacksonville Jaguars": "JAX",
- "Kansas City Chiefs": "KC",
- "Las Vegas Raiders": "LV",
- "Los Angeles Chargers": "LAC",
- "Los Angeles Rams": "LAR",
- "Miami Dolphins": "MIA",
- "Minnesota Vikings": "MIN",
- "New England Patriots": "NE",
- "New Orleans Saints": "NO",
- "New York Giants": "NYG",
- "New York Jets": "NYJ",
- "Philadelphia Eagles": "PHI",
- "Pittsburgh Steelers": "PIT",
- "San Francisco 49ers": "SF",
- "Seattle Seahawks": "SEA",
- "Tampa Bay Buccaneers": "TB",
- "Tennessee Titans": "TEN",
- "Washington Commanders": "WAS"
- }
-
- self.raw_covid_data = {}
- self.raw_covid_data_file_path = Path(data_dir) / "covid_raw_data.json"
-
- self.covid_data = {}
- self.covid_data_file_path = Path(data_dir) / "covid_data.json"
-
- # load preexisting (saved) covid data (if it exists) if refresh=False
- if not self.refresh:
- self.open_covid_data()
-
- # fetch NFL player transactions from the web if not running in offline mode or refresh=True
- if self.refresh or not self.offline:
- if not self.covid_data and self.season >= 2020:
- logger.debug("Retrieving COVID-19 data from the web.")
-
- football_db_endpoint = (
- f"https://www.footballdb.com/transactions/index.html?period={self.season + 1}&period={self.season}"
- )
- headers = {
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/605.1.15 (KHTML, " +
- "like Gecko) Version/13.0 Safari/605.1.15"
- }
- r = requests.get(football_db_endpoint, headers=headers)
- data = r.text
- soup = BeautifulSoup(data, "html.parser")
- self.covid_data = {}
-
- covid_transactions = []
- for row in soup.findAll(attrs={"class": "td w25 rowtitle"}):
-
- transactions_html = row.findNext(attrs={"class": "td w75 td-clear"})
-
- if "covid" in str(transactions_html.text).lower():
- transaction_date = row.findPrevious(attrs={"class": "stacktable-title"}).text
- transaction_team = nfl_team_abbreviations_ref.get(row.b.text)
-
- if datetime.strptime(transaction_date, "%B %d, %Y") <= self.selected_nfl_season_week:
- for transaction in str(transactions_html).split("."):
- if "covid" in transaction.lower():
- for player in BeautifulSoup(transaction, "html.parser").findAll("a"):
- transaction_action = ""
- if "placed" in transaction.lower():
- transaction_action = "add"
- elif "activated" in transaction.lower():
- transaction_action = "remove"
-
- player_transaction = {
- "date": transaction_date,
- "team": transaction_team,
- "action": transaction_action,
- "list": "Reserve/COVID-19",
- "player": player.text
- }
-
- covid_transactions.append(player_transaction)
- self.add_entry(player.text, player_transaction)
-
- self.raw_covid_data = {
- key: {
- "transactions": sorted(
- list(group),
- key=lambda x: datetime.strptime(x["date"], "%B %d, %Y"),
- reverse=True
- )
- } for key, group in itertools.groupby(
- sorted(covid_transactions, key=lambda x: x["team"]),
- lambda x: x["team"]
- )
- }
-
- for team, data in self.raw_covid_data.items():
- data["last_date"] = data["transactions"][0].get("date")
- data["count"] = len(data["transactions"])
- data["transactions"] = {
- key: [
- {
- k: item[k] for k in item if k != "timestamp"
- } for item in sorted(
- list(group),
- key=lambda x: datetime.strptime(x["date"], "%B %d, %Y")
- )
- ] for key, group in itertools.groupby(
- sorted(data["transactions"], key=lambda x: x["action"]),
- lambda x: x["action"]
- )
- }
-
- self.save_covid_data()
-
- # if offline mode, load pre-fetched covid data (only works if you've previously run application with -s flag)
- else:
- if not self.covid_data and self.season >= 2020:
- raise FileNotFoundError(
- f"FILE {self.covid_data_file_path} DOES NOT EXIST. CANNOT RUN LOCALLY WITHOUT HAVING PREVIOUSLY "
- f"SAVED DATA!"
- )
-
- if len(self.covid_data) == 0 and self.season >= 2020:
- logger.warning(
- "NO COVID-19 data was loaded, please check your internet connection or the availability of "
- "\"https://site.web.api.espn.com/apis/site/v2/sports/football/nfl/transactions?region=us&lang=en"
- "&contentorigin=espn\" and try generating a new report.")
- elif self.season < 2020:
- logger.warning("COVID-19 was not a factor during NFL seasons prior to 2020, so metric is being ignored.")
- else:
- logger.info(f"{len(self.covid_data)} players at risk of COVID-19 were loaded")
-
- def open_covid_data(self):
- logger.debug("Loading saved COVID-19 risk data.")
- if Path(self.covid_data_file_path).exists():
- with open(self.covid_data_file_path, "r", encoding="utf-8") as covid_in:
- self.covid_data = dict(json.load(covid_in))
-
- if Path(self.raw_covid_data_file_path).exists():
- with open(self.raw_covid_data_file_path, "r", encoding="utf-8") as covid_raw_in:
- self.raw_covid_data = dict(json.load(covid_raw_in))
-
- def save_covid_data(self):
- if self.save_data:
- logger.debug("Saving COVID-19 risk data.")
- # save report covid data locally
- with open(self.covid_data_file_path, "w", encoding="utf-8") as covid_out:
- json.dump(self.covid_data, covid_out, ensure_ascii=False, indent=2)
-
- # save raw player covid data locally
- with open(self.raw_covid_data_file_path, "w", encoding="utf-8") as covid_raw_out:
- json.dump(self.raw_covid_data, covid_raw_out, ensure_ascii=False, indent=2)
-
- def add_entry(self, player_full_name, player_transaction):
-
- if player_transaction:
- if player_full_name not in self.covid_data:
- self.covid_data[player_full_name] = {
- "team": player_transaction.get("team"),
- "transactions": [player_transaction],
- "last_date": player_transaction.get("date")
- }
- else:
- self.covid_data.get(player_full_name).get("transactions").append(player_transaction)
-
- if datetime.strptime(self.covid_data.get(player_full_name).get("last_date"), "%B %d, %Y") < \
- datetime.strptime(player_transaction.get("date"), "%B %d, %Y"):
- self.covid_data.get(player_full_name)["last_date"] = player_transaction.get("date")
-
- # TODO: incorporate team defenses into COVID-19 risk factor metric
- # player_team_abbr = player_transaction.get("team")
- # if player_team_abbr not in self.covid_data:
- # self.covid_data[player_team_abbr] = {
- # "transactions": [player_transaction],
- # "count": self.raw_covid_data.get(player_team_abbr).get("count"),
- # "last_date": self.raw_covid_data.get(player_team_abbr).get("last_date")
- # }
- # else:
- # self.covid_data.get(player_team_abbr).get("transactions").append(player_transaction)
-
- # noinspection PyUnusedLocal
- def get_player_covid_risk(self, player_full_name, player_team_abbr, player_pos):
-
- team_abbr = player_team_abbr.upper() if player_team_abbr else "?"
- if team_abbr not in nfl_team_abbreviations:
- if team_abbr in nfl_team_abbreviation_conversions.keys():
- team_abbr = nfl_team_abbreviation_conversions[team_abbr]
-
- covid_risk_score = 0
- player_on_covid_list_past = False
- player_on_covid_list_present = False
-
- if team_abbr in self.raw_covid_data.keys():
-
- for transaction in self.raw_covid_data.get(team_abbr).get("transactions").get("add"):
- if player_full_name == transaction.get("player"):
- player_on_covid_list_past = True
- player_on_covid_list_present = True
-
- if self.raw_covid_data.get(team_abbr).get("transactions").get("remove"):
- for transaction in self.raw_covid_data.get(team_abbr).get("transactions").get("remove"):
- if player_full_name == transaction.get("player"):
- player_on_covid_list_past = True
- player_on_covid_list_present = False
-
- if player_on_covid_list_present:
- # add 10 if the player is currently on the Reserve/COVID-19 list
- covid_risk_score += 10
- elif player_on_covid_list_past:
- # add 5 if the player is no longer on the Reserve/COVID-19 list but was previously
- covid_risk_score += 5
-
- # add 1 for every other player on the same team who has been on the Reserve/COVID-19 list
- covid_risk_score += (self.raw_covid_data.get(team_abbr).get("count") - 1)
-
- selected_nfl_season_week = datetime.strptime(
- f"{self.season} {self.week + 36} 1",
- "%G %V %u"
- )
-
- covid_recency = selected_nfl_season_week - datetime.strptime(
- self.raw_covid_data.get(team_abbr).get("last_date"),
- "%B %d, %Y"
- )
- if covid_recency < timedelta(days=14) and not player_on_covid_list_present:
- # add 10 if a teammate was on the Reserve/COVID-19 list within the past 14 days (COVID-19 risk window)
- covid_risk_score += 10
- else:
- # add 10 then subtract 1 for every day beyond 14 days a teammate was last on the Reserve/COVID-19 list
- recency_risk = 10 - (timedelta(days=14) - covid_recency).days
- covid_risk_score += ((10 - recency_risk) if 10 >= recency_risk >= 0 else 0)
-
- else:
- logger.debug(
- f"Team {team_abbr} has no Reserve/COVID-19 transactions. Setting player COVID-19 risk to 0. Run report "
- f"with the -r flag (--refresh-web-data) to refresh all external web data and try again."
- )
-
- return covid_risk_score
-
- def generate_covid_risk_json(self):
- ordered_covid_risk_data = OrderedDict(sorted(self.raw_covid_data.items(), key=lambda k_v: k_v[0]))
- with open(self.raw_covid_data_file_path, mode="w", encoding="utf-8") as covid_data:
- json.dump(ordered_covid_risk_data, covid_data, ensure_ascii=False, indent=2)
-
- def __str__(self):
- return json.dumps(self.covid_data, indent=2, ensure_ascii=False)
-
- def __repr__(self):
- return json.dumps(self.covid_data, indent=2, ensure_ascii=False)
diff --git a/calculate/metrics.py b/calculate/metrics.py
index 7824d6bc..a748e7c8 100644
--- a/calculate/metrics.py
+++ b/calculate/metrics.py
@@ -9,18 +9,16 @@
import numpy as np
from dao.base import BaseLeague, BaseTeam, BaseRecord, BasePlayer
-from report.logger import get_logger
-from utilities.config import AppConfigParser
+from utilities.logger import get_logger
logger = get_logger(__name__, propagate=False)
class CalculateMetrics(object):
- def __init__(self, config: Union[AppConfigParser, None], league_id: Union[str, None],
- playoff_slots: Union[int, None], playoff_simulations: Union[int, None]):
+ def __init__(self, league_id: Union[str, None], playoff_slots: Union[int, None],
+ playoff_simulations: Union[int, None]):
logger.debug("Initializing metrics calculator.")
- self.config: AppConfigParser = config
self.league_id: str = league_id
self.playoff_slots: int = playoff_slots
self.playoff_simulations: int = playoff_simulations
@@ -377,22 +375,6 @@ def get_beef_rank_data(beef_results: List[BaseTeam]) -> List[List[Any]]:
place += 1
return beef_results_data
- @staticmethod
- def get_covid_risk_rank_data(covid_risk_results: List[BaseTeam]) -> List[List[Any]]:
- logger.debug("Creating league COVID-19 risk data.")
-
- covid_risk_data = []
- ndx = 0
- team: BaseTeam
- for team in covid_risk_results:
- ranked_team_name = team.name
- ranked_team_manager = team.manager_str
- ranked_covid_risk = str(team.total_covid_risk)
-
- covid_risk_data.append([ndx, ranked_team_name, ranked_team_manager, ranked_covid_risk])
- ndx += 1
- return covid_risk_data
-
def get_ties_count(self, results_data: List[List[Any]], tie_type: str, break_ties: bool) -> int:
if tie_type == "power_ranking":
diff --git a/calculate/playoff_probabilities.py b/calculate/playoff_probabilities.py
index 32cdba56..4465d836 100644
--- a/calculate/playoff_probabilities.py
+++ b/calculate/playoff_probabilities.py
@@ -17,8 +17,8 @@
import numpy as np
-from report.logger import get_logger
-from utilities.config import AppConfigParser
+from utilities.logger import get_logger
+from utilities.settings import settings
if TYPE_CHECKING:
from dao.base import BaseTeam, BaseMatchup
@@ -105,14 +105,12 @@ def reset_to_base_record(self):
class PlayoffProbabilities(object):
- def __init__(self, config: AppConfigParser, simulations: int, num_weeks: int, num_playoff_slots: int,
- data_dir: Path, num_divisions: int = 0, save_data: bool = False, recalculate: bool = False,
- offline: bool = False):
+ def __init__(self, simulations: int, num_weeks: int, num_playoff_slots: int, data_dir: Path, num_divisions: int = 0,
+ save_data: bool = False, recalculate: bool = False, offline: bool = False):
logger.debug("Initializing playoff probabilities.")
- self.config: AppConfigParser = config
- self.simulations: int = simulations or self.config.getint("Settings", "num_playoff_simulations",
- fallback=100000)
+ self.simulations: int = simulations or settings.num_playoff_simulations
+
self.num_weeks: int = num_weeks
self.num_playoff_slots: int = int(num_playoff_slots)
self.data_dir: Path = data_dir
@@ -189,8 +187,7 @@ def calculate(self, week: int, week_for_report: int, standings: List[BaseTeam],
if self.num_divisions > 0:
sorted_divisions = self.group_by_division(teams_for_playoff_probs)
- num_playoff_slots_per_division_without_leader = self.config.getint(
- "Settings", "num_playoff_slots_per_division", fallback=1) - 1
+ num_playoff_slots_per_division_without_leader = settings.num_playoff_slots_per_division - 1
# pick the teams making the playoffs
division_winners = []
@@ -245,7 +242,7 @@ def calculate(self, week: int, week_for_report: int, standings: List[BaseTeam],
f"Specified number of playoff qualifiers per division "
f"({num_playoff_slots_per_division_without_leader + 1}) exceeds available "
f"league playoff spots. Please correct the value of "
- f"\"num_playoff_slots_per_division\" in \"config.ini\"."
+ f"\"NUM_PLAYOFF_SLOTS_PER_DIVISION\" in \".env\" file."
)
if (len(division_winners) + len(division_qualifiers)) < self.num_playoff_slots:
@@ -288,8 +285,7 @@ def calculate(self, week: int, week_for_report: int, standings: List[BaseTeam],
if self.num_divisions > 0:
sorted_divisions = self.group_by_division(teams_for_playoff_probs)
- num_playoff_slots_per_division_without_leader = self.config.getint(
- "Settings", "num_playoff_slots_per_division", fallback=1) - 1
+ num_playoff_slots_per_division_without_leader = settings.num_playoff_slots_per_division - 1
for division in sorted_divisions.values():
ranked_division = sorted(
@@ -407,11 +403,7 @@ def __repr__(self):
if __name__ == "__main__":
- local_config = AppConfigParser()
- local_config.read(Path(__file__).parent.parent / "config.ini")
-
playoff_probs = PlayoffProbabilities(
- local_config,
simulations=100,
num_weeks=13,
num_playoff_slots=6,
diff --git a/calculate/points_by_position.py b/calculate/points_by_position.py
index ae352be4..d2fad5c8 100644
--- a/calculate/points_by_position.py
+++ b/calculate/points_by_position.py
@@ -5,7 +5,7 @@
from typing import Dict, List, Any
from dao.base import BaseLeague, BaseTeam, BasePlayer
-from report.logger import get_logger
+from utilities.logger import get_logger
logger = get_logger(__name__, propagate=False)
diff --git a/calculate/season_averages.py b/calculate/season_averages.py
index 6216e0a6..b2de1871 100644
--- a/calculate/season_averages.py
+++ b/calculate/season_averages.py
@@ -7,7 +7,7 @@
from calculate.metrics import CalculateMetrics
from report.data import ReportData
-from report.logger import get_logger
+from utilities.logger import get_logger
logger = get_logger(__name__, propagate=False)
@@ -42,7 +42,7 @@ def get_average(self, data: List[List[List[Any]]], key: str, with_percent: bool
index += 1
ordered_average_values = CalculateMetrics(
- None, None, None, None
+ None, None, None
).resolve_season_average_ties(ordered_average_values, with_percent)
ordered_season_average_list = []
diff --git a/compose.build.yaml b/compose.build.yaml
index 521461fc..68a42f6b 100644
--- a/compose.build.yaml
+++ b/compose.build.yaml
@@ -8,3 +8,4 @@ services:
args:
- PYTHON_VERSION_MAJOR=3
- PYTHON_VERSION_MINOR=11
+ - PYTHON_VERSION_PATCH=6
diff --git a/compose.yaml b/compose.yaml
index 75799eb8..d64071c1 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -2,13 +2,13 @@
services:
app:
- image: ghcr.io/uberfastman/fantasy-football-metrics-weekly-report:16.0.1
+ image: ghcr.io/uberfastman/fantasy-football-metrics-weekly-report:17.0.0
platform: linux/amd64
ports:
- "5001:5000"
volumes:
- /etc/localtime:/etc/localtime # sync container timezone with host
- - ./config.ini:/opt/ffmwr/config.ini # mount host config file
+ - ./.env:/opt/ffmwr/.env # mount host .env file
- ./auth:/opt/ffmwr/auth # mount host auth directory
- ./logs:/opt/ffmwr/logs # mount host logs directory
- ./output:/opt/ffmwr/output # mount host output directory
diff --git a/config.template.ini b/config.template.ini
deleted file mode 100644
index 0ffe83d8..00000000
--- a/config.template.ini
+++ /dev/null
@@ -1,118 +0,0 @@
-[Settings]
-; fantasy football platform for which you are running the report: yahoo,espn,sleeper,fleaflicker
-platform = yahoo
-; example public league archive for reference: https://archive.fantasysports.yahoo.com/nfl/2014/729259
-league_id = 729259
-; Yahoo: game_id can be either `nfl`, in which case Yahoo defaults to using the current season, or it can be a specific
-; Yahoo game id for a specific season, such as 331 (2014 NFL season), 380 (2018 NFL season), or 390 (2019 nfl season)
-game_id = nfl
-; Fleaflicker/ESPN: season must be an eligible year for fantasy football
-season = 2021
-; Sleeper/Fleaflicker: default if unable to retrieve the current NFL week from Fox Sports (for Sleeper) or Fleaflicker
-current_week = 1
-; value can be "default" or #, where # is an integer between 1 and 18 defining the chosen week
-week_for_report = default
-; select how many Monte Carlo simulations are used for playoff predictions, keeping in mind that while more simulations
-; improves the quality of the playoff predictions, it also make this step of the report take longer to complete
-num_playoff_simulations = 100000
-; Yahoo: default FAAB since the initial/starting FAAB is not exposed in the API
-initial_faab_budget = 100
-; Fleaflicker: default if number of playoff slots cannot be scraped
-num_playoff_slots = 6
-; number of top ranked teams that make the playoffs from each division (for leagues with divisions)
-num_playoff_slots_per_division = 1
-; Fleaflicker/Sleeper: default if number of regular season weeks cannot be scraped/retrieved
-num_regular_season_weeks = 14
-; multiple teams can be manually disqualified from coaching efficiency eligibility
-; (comma-delimited list with no spaces)
-; example:
-; coaching_efficiency_disqualified_teams = Team One,Team Two
-coaching_efficiency_disqualified_teams =
-
-[Report]
-league_standings = True
-league_playoff_probs = True
-league_median_standings = True
-league_power_rankings = True
-league_z_score_rankings = True
-league_score_rankings = True
-league_coaching_efficiency_rankings = True
-league_luck_rankings = True
-league_optimal_score_rankings = True
-league_bad_boy_rankings = True
-league_beef_rankings = True
-league_covid_risk_rankings = False
-league_weekly_top_scorers = True
-league_weekly_highest_ce = True
-report_time_series_charts = True
-report_team_stats = True
-team_points_by_position_charts = True
-team_bad_boy_stats = True
-team_beef_stats = True
-team_boom_or_bust = True
-; set font and font size of report (defaults to helvetica)
-font = helvetica
-; supported fonts (comma-delimited list with no spaces)
-supported_fonts = helvetica,times,symbola,opensansemoji,sketchcollege,leaguegothic
-; set base font size (certain report element fonts resize dynamically based on the base font size)
-font_size = 12
-; specify player headshot image quality in percent (default: 75%)
-; higher quality (-> 100%) results in the PDF report file being larger
-image_quality = 75
-; specify max number of characters to display for any given data cell in the report tables
-max_data_chars = 24
-
-[Configuration]
-; logger output level: notset, debug, info, warning, error, critical
-log_level = info
-; output directories can be set to store your saved data and generated reports wherever you want
-data_dir = output/data
-output_dir = output/reports
-; prohibited player statuses to check team coaching efficiency eligibility (if dq_ce = True)
-; (comma-delimited list with no spaces)
-; OPTIONS:
-; D: Doubtful
-; NA: Inactive: Coach's Decision or Not on Roster
-; IR: Injured Reserve
-; IR-R: Injured Reserve - Designated for Return
-; NFI: Non-Football Injury
-; NFI-A: Non-Football Injury (Active)
-; NFI-R: Non-Football Injury (Reserve)
-; O: Out
-; PUP-P: Physically Unable to Perform (Preseason)
-; PUP-R: Physically Unable to Perform (Regular Season)
-; Q: Questionable
-; COVID-19: Reserve: COVID-19
-; CEL: Reserve: Commissioner Exempt List
-; DNR: Reserve: Did Not Report
-; EX: Reserve: Exemption
-; RET: Reserve: Retired
-; SUSP: Suspended
-prohibited_statuses = NA,IR,IR-R,NFI,NFI-R,O,PUP-P,PUP-R,COVID-19,CEL,DNR,EX,RET,SUSP,INACTIVE,Out,Reserve-Sus,Reserve-CEL,Reserve-Ret,Reserve-Ex
-
-[Yahoo]
-yahoo_auth_dir = auth/yahoo
-
-[ESPN]
-espn_auth_dir = auth/espn
-
-[CBS]
-cbs_auth_dir = auth/cbs
-
-[Drive]
-; change google_drive_upload to True/False to turn on/off uploading of the report to Google Drive
-google_drive_upload = False
-google_drive_auth_token = auth/google/token.json
-google_drive_folder_path_default = Fantasy_Football
-google_drive_folder_path =
-google_drive_reupload_file = resources/files/example_report.pdf
-
-[Slack]
-; change post_to_slack to True/False to turn on/off posting of the report to Slack
-post_to_slack = False
-slack_auth_token = auth/slack/token.json
-; options for post_or_file: post (if you wish to post a link to the report), file (if you wish to post the report PDF)
-post_or_file = file
-slack_channel = sports
-notify_channel = False
-repost_file = resources/files/example_report.pdf
diff --git a/dao/base.py b/dao/base.py
index c66ef5d0..18346c80 100644
--- a/dao/base.py
+++ b/dao/base.py
@@ -10,8 +10,6 @@
from calculate.bad_boy_stats import BadBoyStats
from calculate.beef_stats import BeefStats
-from calculate.covid_risk import CovidRisk
-from utilities.config import AppConfigParser
from calculate.playoff_probabilities import PlayoffProbabilities
@@ -87,12 +85,11 @@ def to_json(self):
class BaseLeague(FantasyFootballReportObject):
- def __init__(self, config: AppConfigParser, data_dir: Path, league_id: str, season: int, week_for_report: int,
+ def __init__(self, data_dir: Path, league_id: str, season: int, week_for_report: int,
save_data: bool = True, offline: bool = False):
super().__init__()
# attributes set during instantiation
- self.config: AppConfigParser = config
self.data_dir: Path = data_dir
self.league_id: str = league_id
self.season: int = season
@@ -235,7 +232,6 @@ def get_playoff_probs(self, save_data: bool = False, playoff_prob_sims: int = No
recalculate: bool = True) -> PlayoffProbabilities:
# TODO: UPDATE USAGE OF recalculate PARAM (could use self.offline)
return PlayoffProbabilities(
- self.config,
playoff_prob_sims,
self.num_regular_season_weeks,
self.num_playoff_slots,
@@ -262,17 +258,6 @@ def get_beef_stats(self, save_data: bool = False, offline: bool = False, refresh
refresh=refresh
)
- def get_covid_risk(self, save_data: bool = False, offline: bool = False, refresh: bool = False) -> CovidRisk:
- return CovidRisk(
- self.config,
- Path(self.data_dir) / str(self.season) / self.league_id,
- season=self.season,
- week=self.week_for_report,
- save_data=save_data,
- offline=offline,
- refresh=refresh
- )
-
class BaseMatchup(FantasyFootballReportObject):
@@ -327,7 +312,6 @@ def __init__(self):
self.worst_offense_score: int = 0
self.total_weight: float = 0.0
self.tabbu: float = 0
- self.total_covid_risk: int = 0
self.positions_filled_active: List[str] = []
self.coaching_efficiency: Union[float, str] = 0.0
self.luck: float = 0
@@ -648,7 +632,6 @@ def __init__(self):
self.bad_boy_num_offenders: int = 0
self.weight: int = 0
self.tabbu: float = 0.0
- self.covid_risk: float = 0.0
class BaseStat(FantasyFootballReportObject):
diff --git a/dao/platforms/__init__.py b/dao/platforms/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/dao/platforms/base/base.py b/dao/platforms/base/base.py
index d07e91f4..c21f8656 100644
--- a/dao/platforms/base/base.py
+++ b/dao/platforms/base/base.py
@@ -13,8 +13,7 @@
from requests.exceptions import HTTPError
from dao.base import BaseLeague
-from report.logger import get_logger
-from utilities.config import AppConfigParser
+from utilities.logger import get_logger
from utilities.utils import format_platform_display
logger = get_logger(__name__, propagate=False)
@@ -29,7 +28,6 @@ class BaseLeagueData(ABC):
def __init__(self,
platform: str,
base_url: Union[str, None],
- config: AppConfigParser,
base_dir: Path,
data_dir: Path,
league_id: str,
@@ -50,13 +48,13 @@ def __init__(self,
self.start_week = start_week or 1
# retrieve current NFL week
- self.current_week: int = get_current_nfl_week_function(config, offline)
+ self.current_week: int = get_current_nfl_week_function(offline)
# validate user selection of week for which to generate report
- week_for_report = week_validation_function(config, week_for_report, self.current_week, season)
+ week_for_report = week_validation_function(week_for_report, self.current_week, season)
logger.debug(f"Initializing {self.platform_display} league.")
- self.league: BaseLeague = BaseLeague(config, data_dir, league_id, season, week_for_report, save_data, offline)
+ self.league: BaseLeague = BaseLeague(data_dir, league_id, season, week_for_report, save_data, offline)
# create full directory path if any directories in it do not already exist
if not Path(self.league.data_dir).exists():
diff --git a/dao/platforms/cbs.py b/dao/platforms/cbs.py
index 28744b74..26ce5668 100644
--- a/dao/platforms/cbs.py
+++ b/dao/platforms/cbs.py
@@ -9,8 +9,8 @@
from dao.base import BaseLeague, BaseMatchup, BaseTeam, BaseManager, BaseRecord, BasePlayer, BaseStat
from dao.platforms.base.base import BaseLeagueData
-from report.logger import get_logger
-from utilities.config import AppConfigParser
+from utilities.logger import get_logger
+from utilities.settings import settings
logger = get_logger(__name__, propagate=False)
@@ -22,13 +22,12 @@
# noinspection DuplicatedCode
class LeagueData(BaseLeagueData):
- def __init__(self, config: AppConfigParser, base_dir: Path, data_dir: Path, league_id: str, season: int,
+ def __init__(self, base_dir: Path, data_dir: Path, league_id: str, season: int,
start_week: int, week_for_report: int, get_current_nfl_week_function: Callable,
week_validation_function: Callable, save_data: bool = True, offline: bool = False):
super().__init__(
"CBS",
f"https://{league_id}.football.cbssports.com",
- config,
base_dir,
data_dir,
league_id,
@@ -60,9 +59,7 @@ def __init__(self, config: AppConfigParser, base_dir: Path, data_dir: Path, leag
def check_auth(self) -> str:
auth_json = None
- cbs_auth_file = (
- Path(self.base_dir) / self.league.config.get("CBS", "cbs_auth_dir") / "private.json"
- )
+ cbs_auth_file = Path(self.base_dir) / settings.platform_settings.cbs_auth_dir_local_path / "private.json"
if Path(cbs_auth_file).is_file():
with open(cbs_auth_file, "r") as auth_file:
auth_json = json.load(auth_file)
@@ -105,8 +102,8 @@ def build_api_url(self, route: str, additional_parameters: Dict[str, Any] = None
return f"{self.api_base_url}{route}?{api_url_query_parameters}"
@staticmethod
- def extract_integer(input_str_with_embedded_int: str):
- return int("".join(filter(str.isdigit, input_str_with_embedded_int)))
+ def extract_integer(input_with_embedded_int: Any):
+ return int("".join(filter(str.isdigit, str(input_with_embedded_int))))
def map_data_to_base(self) -> BaseLeague:
logger.debug(f"Retrieving {self.platform_display} league data and mapping it to base objects.")
diff --git a/dao/platforms/espn.py b/dao/platforms/espn.py
index f1031faf..1932a234 100644
--- a/dao/platforms/espn.py
+++ b/dao/platforms/espn.py
@@ -28,8 +28,8 @@
from dao.base import BaseMatchup, BaseTeam, BaseRecord, BaseManager, BasePlayer, BaseStat
from dao.platforms.base.base import BaseLeagueData
-from report.logger import get_logger
-from utilities.config import AppConfigParser
+from utilities.logger import get_logger
+from utilities.settings import settings
colorama.init()
@@ -49,13 +49,12 @@
# noinspection DuplicatedCode
class LeagueData(BaseLeagueData):
- def __init__(self, config: AppConfigParser, base_dir: Path, data_dir: Path, league_id: str, season: int,
+ def __init__(self, base_dir: Path, data_dir: Path, league_id: str, season: int,
start_week: int, week_for_report: int, get_current_nfl_week_function: Callable,
week_validation_function: Callable, save_data: bool = True, offline: bool = False):
super().__init__(
"ESPN",
"https://fantasy.espn.com",
- config,
base_dir,
data_dir,
league_id,
@@ -69,7 +68,7 @@ def __init__(self, config: AppConfigParser, base_dir: Path, data_dir: Path, leag
)
espn_auth_json = None
- espn_auth_file = Path(base_dir) / config.get("ESPN", "espn_auth_dir") / "private.json"
+ espn_auth_file = Path(base_dir) / settings.platform_settings.espn_auth_dir_local_path / "private.json"
if Path(espn_auth_file).is_file():
with open(espn_auth_file, "r") as auth:
espn_auth_json = json.load(auth)
@@ -323,8 +322,8 @@ def map_data_to_base(self):
matchups_by_week = {}
matchups_json_by_week = {}
median_score_by_week = {}
- for week_for_matchups in range(1, int(espn_league.settings.reg_season_count) + 1):
- matchups_by_week[str(week_for_matchups)] = espn_league.box_scores(int(week_for_matchups))
+ for week_for_matchups in range(self.start_week, int(espn_league.settings.reg_season_count) + 1):
+ matchups_by_week[str(week_for_matchups)] = espn_league.box_scores(week_for_matchups)
matchups_json_by_week[str(week_for_matchups)] = espn_league.box_data_json
if int(week_for_matchups) <= self.league.week_for_report:
@@ -506,7 +505,7 @@ def map_data_to_base(self):
logger.debug("Getting ESPN rosters by week data.")
rosters_by_week = {}
rosters_json_by_week = {}
- for week_for_rosters in range(1, self.league.week_for_report + 1):
+ for week_for_rosters in range(self.start_week, self.league.week_for_report + 1):
team_rosters = {}
for matchup in matchups_by_week[str(week_for_rosters)]:
team_rosters[matchup.home_team.team_id] = matchup.home_lineup
diff --git a/dao/platforms/fleaflicker.py b/dao/platforms/fleaflicker.py
index 2890a254..4ed4e04b 100644
--- a/dao/platforms/fleaflicker.py
+++ b/dao/platforms/fleaflicker.py
@@ -16,8 +16,8 @@
from dao.base import BaseMatchup, BaseTeam, BaseRecord, BaseManager, BasePlayer, BaseStat
from dao.platforms.base.base import BaseLeagueData
-from report.logger import get_logger
-from utilities.config import AppConfigParser
+from utilities.logger import get_logger
+from utilities.settings import settings
logger = get_logger(__name__, propagate=False)
@@ -28,13 +28,12 @@
# noinspection DuplicatedCode
class LeagueData(BaseLeagueData):
- def __init__(self, config: AppConfigParser, base_dir: Union[Path, None], data_dir: Path, league_id: str,
+ def __init__(self, base_dir: Union[Path, None], data_dir: Path, league_id: str,
season: int, start_week: int, week_for_report: int, get_current_nfl_week_function: Callable,
week_validation_function: Callable, save_data: bool = True, offline: bool = False):
super().__init__(
"Fleaflicker",
f"https://www.fleaflicker.com",
- config,
base_dir,
data_dir,
league_id,
@@ -134,15 +133,11 @@ def map_data_to_base(self):
# TODO: figure out how to get total number of regular season weeks when league has no playoffs
self.league.num_regular_season_weeks = 18 if int(self.league.season) > 2020 else 17
else:
- self.league.num_regular_season_weeks = self.league.config.getint(
- "Settings", "num_regular_season_weeks", fallback=14
- )
+ self.league.num_regular_season_weeks = settings.num_regular_season_weeks
break
else:
- self.league.num_playoff_slots = self.league.config.getint("Settings", "num_playoff_slots", fallback=6)
- self.league.num_regular_season_weeks = self.league.config.getint(
- "Settings", "num_regular_season_weeks", fallback=14
- )
+ self.league.num_playoff_slots = settings.num_playoff_slots
+ self.league.num_regular_season_weeks = settings.num_regular_season_weeks
# TODO: how to get league rules for LAST YEAR from Fleaflicker API
league_rules = self.query(
diff --git a/dao/platforms/sleeper.py b/dao/platforms/sleeper.py
index 9e16d95c..5ad34a68 100644
--- a/dao/platforms/sleeper.py
+++ b/dao/platforms/sleeper.py
@@ -14,8 +14,8 @@
from dao.base import BaseMatchup, BaseTeam, BaseRecord, BaseManager, BasePlayer, BaseStat
from dao.platforms.base.base import BaseLeagueData
-from report.logger import get_logger
-from utilities.config import AppConfigParser
+from utilities.logger import get_logger
+from utilities.settings import settings
logger = get_logger(__name__, propagate=False)
@@ -26,13 +26,12 @@
# noinspection DuplicatedCode
class LeagueData(BaseLeagueData):
- def __init__(self, config: AppConfigParser, base_dir: Union[Path, None], data_dir: Path, league_id: str,
+ def __init__(self, base_dir: Union[Path, None], data_dir: Path, league_id: str,
season: int, start_week: int, week_for_report: int, get_current_nfl_week_function: Callable,
week_validation_function: Callable, save_data: bool = True, offline: bool = False):
super().__init__(
"Sleeper",
f"https://api.sleeper.app",
- config,
base_dir,
data_dir,
league_id,
@@ -144,7 +143,7 @@ def map_data_to_base(self):
num_regular_season_weeks: int = (
(int(league_settings.get("playoff_week_start")) - 1)
if league_settings.get("playoff_week_start") > 0
- else int(self.league.config.get("Settings", "num_regular_season_weeks"))
+ else settings.num_regular_season_weeks
)
league_managers = {
diff --git a/dao/platforms/yahoo.py b/dao/platforms/yahoo.py
index cdc2ebe0..29eb1036 100644
--- a/dao/platforms/yahoo.py
+++ b/dao/platforms/yahoo.py
@@ -14,8 +14,8 @@
from dao.base import BaseMatchup, BaseTeam, BaseRecord, BaseManager, BasePlayer, BaseStat
from dao.platforms.base.base import BaseLeagueData
-from report.logger import get_logger
-from utilities.config import AppConfigParser
+from utilities.logger import get_logger
+from utilities.settings import settings
logger = get_logger(__name__, propagate=False)
@@ -27,14 +27,13 @@
# noinspection DuplicatedCode
class LeagueData(BaseLeagueData):
- def __init__(self, config: AppConfigParser, base_dir: Path, data_dir: Path, game_id: Union[str, int],
+ def __init__(self, base_dir: Path, data_dir: Path, game_id: Union[str, int],
league_id: str, season: int, start_week: int, week_for_report: int,
get_current_nfl_week_function: Callable, week_validation_function: Callable, save_data: bool = True,
offline: bool = False):
super().__init__(
"Yahoo",
None,
- config,
base_dir,
data_dir,
league_id,
@@ -47,14 +46,14 @@ def __init__(self, config: AppConfigParser, base_dir: Path, data_dir: Path, game
offline
)
- self.game_id = game_id if game_id else self.league.config.get("Settings", "game_id")
+ self.game_id = game_id or settings.platform_settings.yahoo_game_id
self.yahoo_data = Data(self.league.data_dir, save_data=self.league.save_data, dev_offline=self.league.offline)
- yahoo_auth_dir = Path(self.base_dir) / Path(self.league.config.get("Yahoo", "yahoo_auth_dir"))
+ yahoo_auth_dir = Path(self.base_dir) / settings.platform_settings.yahoo_auth_dir_local_path
self.yahoo_query = YahooFantasySportsQuery(
- yahoo_auth_dir, self.league.league_id, game_id=self.game_id, offline=self.league.offline,
- browser_callback=False
+ yahoo_auth_dir, self.league.league_id, game_code=self.game_id, game_id=self.game_id,
+ offline=self.league.offline, browser_callback=False
)
def get_player_data(self, player_key: str, week: int = None):
@@ -128,7 +127,7 @@ def map_data_to_base(self):
self.league.median_score = 0
self.league.is_faab = bool(league_info.settings.uses_faab)
if self.league.is_faab:
- self.league.faab_budget = self.league.config.getint("Settings", "initial_faab_budget", fallback=100)
+ self.league.faab_budget = settings.platform_settings.yahoo_initial_faab_budget or 100
self.league.url = league_info.url
self.league.player_data_by_week_function = self.get_player_data
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 53572268..9588fbd0 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,8 +1,9 @@
ARG PYTHON_VERSION_MAJOR=$PYTHON_VERSION_MAJOR
ARG PYTHON_VERSION_MINOR=$PYTHON_VERSION_MINOR
+ARG PYTHON_VERSION_PATCH=$PYTHON_VERSION_PATCH
# set base image
-FROM --platform=linux/amd64 python:${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}-slim
+FROM --platform=linux/amd64 python:${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}.${PYTHON_VERSION_PATCH}-slim
LABEL "org.opencontainers.image.source"="https://github.com/uberfastman/fantasy-football-metrics-weekly-report"
diff --git a/integrations/drive_integration.py b/integrations/drive_integration.py
index 7aebeea8..36259ee7 100644
--- a/integrations/drive_integration.py
+++ b/integrations/drive_integration.py
@@ -6,14 +6,14 @@
import datetime
import logging
from pathlib import Path
-from typing import List
+from typing import List, Union
from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
from pydrive.files import GoogleDriveFile
-from report.logger import get_logger
-from utilities.config import AppConfigParser
+from utilities.logger import get_logger
+from utilities.settings import settings
logger = get_logger(__name__, propagate=False)
@@ -25,18 +25,17 @@
class GoogleDriveUploader(object):
- def __init__(self, filename: str, config: AppConfigParser):
+ def __init__(self, file: Union[str, Path]):
logger.debug("Initializing Google Drive uploader.")
- project_dir = Path(__file__).parents[1]
+ project_dir: Path = Path(__file__).parents[1]
logger.debug("Authenticating with Google Drive.")
- self.filename: Path = Path(project_dir) / filename
- self.config: AppConfigParser = config
+ self.file_path: Path = Path(project_dir) / file
self.gauth: GoogleAuth = GoogleAuth()
- auth_token = Path(project_dir) / Path(self.config.get("Drive", "google_drive_auth_token"))
+ auth_token = project_dir / settings.integration_settings.google_drive_auth_token_local_path
# Try to load saved client credentials
self.gauth.LoadCredentialsFile(auth_token)
@@ -62,9 +61,10 @@ def upload_file(self, test: bool = False) -> str:
root_folders = drive.ListFile(
{"q": "'root' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false"}).GetList()
- google_drive_folder_path_default = self.config.get("Drive", "google_drive_folder_path_default")
- google_drive_folder_path = Path(self.config.get(
- "Drive", "google_drive_folder_path", fallback=google_drive_folder_path_default)).parts
+ google_drive_folder_path_default = settings.integration_settings.google_drive_default_folder_path
+ google_drive_folder_path = Path(
+ settings.integration_settings.google_drive_folder_path or google_drive_folder_path_default
+ ).parts
google_drive_root_folder_id = self.make_root_folder(
drive,
@@ -99,7 +99,7 @@ def upload_file(self, test: bool = False) -> str:
}).GetList()
# Check for season folder and create it if it does not exist
- season_folder_name = Path(self.filename).parts[-3]
+ season_folder_name = Path(self.file_path).parts[-3]
season_folder_id = self.make_parent_folder(
drive,
@@ -116,7 +116,7 @@ def upload_file(self, test: bool = False) -> str:
}).GetList()
# Check for league folder and create it if it does not exist
- league_folder_name = Path(self.filename).parts[-2].replace("-", "_")
+ league_folder_name = self.file_path.parts[-2].replace("-", "_")
league_folder_id = self.make_parent_folder(
drive,
self.check_file_existence(league_folder_name, season_folder_content_folders, season_folder_id),
@@ -131,12 +131,12 @@ def upload_file(self, test: bool = False) -> str:
}).GetList()
# Check for league report and create it if it does not exist
- report_file_name = Path(self.filename).parts[-1]
+ report_file_name = self.file_path.parts[-1]
report_file = self.check_file_existence(report_file_name, league_folder_content_pdfs, league_folder_id)
else:
all_pdfs = drive.ListFile({"q": "mimeType='application/pdf' and trashed=false"}).GetList()
- report_file_name = Path(self.filename).parts[-1]
+ report_file_name = self.file_path.parts[-1]
report_file = self.check_file_existence(report_file_name, all_pdfs, "root")
league_folder_id = "root"
@@ -154,7 +154,7 @@ def upload_file(self, test: bool = False) -> str:
]
}
)
- upload_file.SetContentFile(self.filename)
+ upload_file.SetContentFile(self.file_path)
# Upload the file.
upload_file.Upload()
@@ -236,12 +236,10 @@ def make_parent_folder(drive: GoogleDrive, folder: GoogleDriveFile, folder_name:
if __name__ == "__main__":
- local_config = AppConfigParser()
- local_config.read(Path(__file__).parents[1] / "config.ini")
- reupload_file = local_config.get("Drive", "google_drive_reupload_file")
+ reupload_file = settings.integration_settings.google_drive_reupload_file_local_path
logger.info(f"Re-uploading {Path(reupload_file).name} ({Path(reupload_file).parts[2]}) to Google Drive...")
- google_drive_uploader = GoogleDriveUploader(reupload_file, local_config)
+ google_drive_uploader = GoogleDriveUploader(reupload_file)
upload_message = google_drive_uploader.upload_file()
logger.info(upload_message)
diff --git a/integrations/slack_integration.py b/integrations/slack_integration.py
index ef2533fc..d4812d7f 100644
--- a/integrations/slack_integration.py
+++ b/integrations/slack_integration.py
@@ -13,8 +13,8 @@
from slack.web.base_client import SlackResponse
from slack.web.client import WebClient
-from report.logger import get_logger
-from utilities.config import AppConfigParser
+from utilities.logger import get_logger
+from utilities.settings import settings
logger = get_logger(__name__, propagate=False)
@@ -24,16 +24,14 @@
class SlackMessenger(object):
- def __init__(self, config):
+ def __init__(self):
logger.debug("Initializing Slack messenger.")
- self.project_dir = Path(__file__).parent.parent
-
- self.config: AppConfigParser = config
+ self.project_dir: Path = Path(__file__).parent.parent
logger.debug("Authenticating with Slack.")
- auth_token = Path(self.project_dir) / self.config.get("Slack", "slack_auth_token")
+ auth_token = self.project_dir / settings.integration_settings.slack_auth_token_local_path
with open(auth_token, "r") as token_file:
# more information at https://api.slack.com/web#authentication
@@ -144,7 +142,7 @@ def post_to_selected_slack_channel(self, message: str) -> Union[Future, SlackRes
try:
return self.sc.chat_postMessage(
- channel=self.get_channel_id(self.config.get("Slack", "slack_channel")),
+ channel=self.get_channel_id(settings.integration_settings.slack_channel),
text="\n" + message,
username="ff-report",
# icon_emoji=":football:"
@@ -164,17 +162,17 @@ def upload_file_to_selected_slack_channel(self, upload_file: str) -> Union[Futur
f"\nFantasy Football Report for {league_name}\nGenerated {datetime.datetime.now():%Y-%b-%d %H:%M:%S}\n"
)
- upload_file = Path(self.project_dir) / upload_file
+ upload_file = self.project_dir / upload_file
with open(upload_file, "rb") as uf:
- if self.config.getboolean("Slack", "notify_channel"):
+ if settings.integration_settings.slack_channel_notify_bool:
# post message with no additional content to trigger @here
self.post_to_selected_slack_channel("")
file_to_upload = uf.read()
# noinspection PyTypeChecker
response = self.sc.files_upload(
- channels=self.get_channel_id(self.config.get("Slack", "slack_channel")),
+ channels=self.get_channel_id(settings.integration_settings.slack_channel),
filename=file_name,
filetype=file_type,
file=file_to_upload,
@@ -187,11 +185,9 @@ def upload_file_to_selected_slack_channel(self, upload_file: str) -> Union[Futur
if __name__ == "__main__":
- local_config = AppConfigParser()
- local_config.read(Path(__file__).parent.parent / "config.ini")
- repost_file = Path(__file__).parent.parent / local_config.get("Slack", "repost_file")
+ repost_file = Path(__file__).parent.parent / settings.integration_settings.slack_repost_file_local_path
- post_to_slack = SlackMessenger(local_config)
+ post_to_slack = SlackMessenger()
# general slack integration testing
logger.info(f"{json.dumps(post_to_slack.api_test().data, indent=2)}")
diff --git a/main.py b/main.py
index cee24443..77911492 100644
--- a/main.py
+++ b/main.py
@@ -16,9 +16,9 @@
from integrations.drive_integration import GoogleDriveUploader
from integrations.slack_integration import SlackMessenger
from report.builder import FantasyFootballReport
-from report.logger import get_logger
-from utilities.config import AppConfigParser
-from utilities.app import check_for_updates, get_valid_config
+from utilities.app import check_for_updates
+from utilities.logger import get_logger
+from utilities.settings import settings
colorama.init()
@@ -60,9 +60,9 @@ def main(argv):
"\n"
" python main.py [optional_parameters]\n"
"\n"
- " Options:\n"
+ " Options:\n"
" -h, --help Print command line usage message.\n"
- " -d, --use-default Run the report using the default configuration without user input prompts.\n"
+ " -d, --use-default Run the report using the default settings without user input prompts.\n"
"\n"
" Generate report:\n"
" -f, --fantasy-platform Fantasy football platform on which league for report is hosted. Currently supports: \"yahoo\", \"fleaflicker\" \n"
@@ -72,8 +72,7 @@ def main(argv):
" -g, --game-id Chosen fantasy game id for which to generate report. Defaults to \"nfl\", which is interpreted as the current season if using Yahoo.\n"
" -y, --year Chosen year (season) of the league for which a report is being generated.\n"
"\n"
- " Configuration:\n"
- " -c, --config-file System file path (including file name) for .ini file to be used for configuration.\n"
+ " Settings:\n"
" -s, --save-data Save all retrieved data locally for faster future report generation.\n"
" -r, --refresh-web-data Refresh all web data from external APIs (such as bad boy and beef data).\n"
" -p, --playoff-prob-sims Number of Monte Carlo playoff probability simulations to run.\n"
@@ -86,7 +85,7 @@ def main(argv):
)
try:
- opts, args = getopt.getopt(argv, "hdc:f:l:w:k:g:y:srp:bqto")
+ opts, args = getopt.getopt(argv, "hdf:l:w:k:g:y:srp:bqot")
except getopt.GetoptError:
print(usage_str)
sys.exit(2)
@@ -98,7 +97,7 @@ def main(argv):
print(usage_str)
sys.exit(0)
- # automatically run the report using the default configuration without user input prompts
+ # automatically run the report using the default settings without user input prompts
elif opt in ("-d", "--use-default"):
options_dict["use_default"] = True
@@ -120,9 +119,7 @@ def main(argv):
elif opt in ("-y", "--year"):
options_dict["year"] = int(arg)
- # report configuration
- elif opt in ("-c", "--config-file"):
- options_dict["config_file"] = arg
+ # report settings
elif opt in ("-s", "--save-data"):
options_dict["save_data"] = True
elif opt in ("-r", "--refresh-web-data"):
@@ -143,15 +140,14 @@ def main(argv):
return options_dict
-def select_league(config: AppConfigParser, use_default: bool, week: int, start_week: int, platform: str,
+def select_league(use_default: bool, week: int, start_week: int, platform: str,
league_id: Union[str, None], game_id: Union[int, str], season: int, refresh_web_data: bool,
playoff_prob_sims: int, break_ties: bool, dq_ce: bool, save_data: bool,
offline: bool, test: bool) -> FantasyFootballReport:
-
# set "use default" environment variable for access by fantasy football platforms
if use_default:
os.environ["USE_DEFAULT"] = "1"
-
+
if not league_id:
if not use_default:
time.sleep(0.25)
@@ -178,7 +174,6 @@ def select_league(config: AppConfigParser, use_default: bool, week: int, start_w
game_id=game_id,
season=season,
start_week=start_week,
- config=config,
refresh_web_data=refresh_web_data,
playoff_prob_sims=playoff_prob_sims,
break_ties=break_ties,
@@ -206,7 +201,6 @@ def select_league(config: AppConfigParser, use_default: bool, week: int, start_w
game_id=game_id,
season=season,
start_week=start_week,
- config=config,
refresh_web_data=refresh_web_data,
playoff_prob_sims=playoff_prob_sims,
break_ties=break_ties,
@@ -217,8 +211,10 @@ def select_league(config: AppConfigParser, use_default: bool, week: int, start_w
)
except IndexError:
logger.error("The league ID you have selected is not valid.")
- select_league(config, use_default, week, start_week, platform, None, game_id, season,
- refresh_web_data, playoff_prob_sims, break_ties, dq_ce, save_data, offline, test)
+ select_league(
+ use_default, week, start_week, platform, None, game_id, season, refresh_web_data,
+ playoff_prob_sims, break_ties, dq_ce, save_data, offline, test
+ )
elif selection == "selected":
if not week:
@@ -233,7 +229,6 @@ def select_league(config: AppConfigParser, use_default: bool, week: int, start_w
game_id=game_id,
season=season,
start_week=start_week,
- config=config,
refresh_web_data=refresh_web_data,
playoff_prob_sims=playoff_prob_sims,
break_ties=break_ties,
@@ -245,8 +240,10 @@ def select_league(config: AppConfigParser, use_default: bool, week: int, start_w
else:
logger.warning("You must select either \"y\" or \"n\".")
time.sleep(0.25)
- select_league(config, use_default, week, start_week, platform, None, game_id, season, refresh_web_data,
- playoff_prob_sims, break_ties, dq_ce, save_data, offline, test)
+ select_league(
+ use_default, week, start_week, platform, None, game_id, season, refresh_web_data,
+ playoff_prob_sims, break_ties, dq_ce, save_data, offline, test
+ )
def select_week(use_default: bool = False) -> Union[int, None]:
@@ -285,19 +282,12 @@ def select_week(use_default: bool = False) -> Union[int, None]:
if __name__ == "__main__":
options = main(sys.argv[1:])
- logger.debug(f"Fantasy football metrics weekly report app run configuration options:\n{options}")
-
- # set local config (check for existence & access, create config.ini if it does not exist/stop app if inaccessible)
- if options.get("config_file"):
- configuration = get_valid_config(options.get("config_file"))
- else:
- configuration = get_valid_config()
+ logger.debug(f"Fantasy football metrics weekly report app settings options:\n{options}")
# check to see if the current app is behind any commits, and provide option to update and re-run if behind
up_to_date = check_for_updates(options.get("use_default", False))
report = select_league(
- configuration,
options.get("use_default", False),
options.get("week", None),
options.get("start_week", None),
@@ -314,23 +304,23 @@ def select_week(use_default: bool = False) -> Union[int, None]:
options.get("test", False))
report_pdf = report.create_pdf_report()
- upload_file_to_google_drive = configuration.getboolean("Drive", "google_drive_upload")
+ upload_file_to_google_drive = settings.integration_settings.google_drive_upload_bool
upload_message = ""
if upload_file_to_google_drive:
if not options.get("test", False):
# upload pdf to google drive
- google_drive_uploader = GoogleDriveUploader(report_pdf, configuration)
+ google_drive_uploader = GoogleDriveUploader(report_pdf)
upload_message = google_drive_uploader.upload_file()
logger.info(upload_message)
else:
logger.info("Test report NOT uploaded to Google Drive.")
- post_to_slack = configuration.getboolean("Slack", "post_to_slack")
+ post_to_slack = settings.integration_settings.slack_post_bool
if post_to_slack:
if not options.get("test", False):
# post pdf or link to pdf to slack
- slack_messenger = SlackMessenger(configuration)
- post_or_file = configuration.get("Slack", "post_or_file")
+ slack_messenger = SlackMessenger()
+ post_or_file = settings.integration_settings.slack_post_or_file
if post_or_file == "post":
# post shareable link to uploaded Google Drive pdf on slack
@@ -340,8 +330,8 @@ def select_week(use_default: bool = False) -> Union[int, None]:
slack_response = slack_messenger.upload_file_to_selected_slack_channel(report_pdf)
else:
logger.warning(
- f"You have configured \"config.ini\" with unsupported Slack setting: "
- f"post_or_file = {post_or_file}. Please choose \"post\" or \"file\" and try again."
+ f"The \".env\" file contains unsupported Slack setting: "
+ f"SLACK_POST_OR_FILE={post_or_file}. Please choose \"post\" or \"file\" and try again."
)
sys.exit(1)
if slack_response.get("ok"):
diff --git a/report/builder.py b/report/builder.py
index 5db49053..fa04d612 100644
--- a/report/builder.py
+++ b/report/builder.py
@@ -13,9 +13,10 @@
from calculate.season_averages import SeasonAverageCalculator
from dao.base import BaseLeague, BaseTeam
from report.data import ReportData
-from report.logger import get_logger
from report.pdf.generator import PdfGenerator
from utilities.app import league_data_factory, patch_http_connection_pool
+from utilities.logger import get_logger
+from utilities.settings import settings
from utilities.utils import format_platform_display
logger = get_logger(__name__, propagate=False)
@@ -29,7 +30,6 @@ def __init__(self,
game_id=None,
season=None,
start_week=None,
- config=None,
refresh_web_data=False,
playoff_prob_sims=None,
break_ties=False,
@@ -42,29 +42,28 @@ def __init__(self,
patch_http_connection_pool(maxsize=100)
- # config vars
- self.config = config
+ # settings
base_dir = Path(__file__).parent.parent
- self.data_dir = base_dir / Path(self.config.get("Configuration", "data_dir"))
+ self.data_dir = base_dir / settings.data_dir_local_path
if platform:
self.platform: str = platform
self.platform_display: str = format_platform_display(self.platform)
else:
- self.platform: str = self.config.get("Settings", "platform")
+ self.platform: str = settings.platform
self.platform_display: str = format_platform_display(self.platform)
if league_id:
- self.league_id = str(league_id)
+ self.league_id: str = str(league_id)
else:
- self.league_id = str(self.config.get("Settings", "league_id"))
+ self.league_id: str = settings.league_id
# TODO: game_id for Yahoo Fantasy Football can be int or str (YFPY expects int, with str for game_code)
if game_id:
self.game_id = game_id
else:
- self.game_id = self.config.get("Settings", "game_id")
+ self.game_id = settings.platform_settings.yahoo_game_id
if season:
- self.season = int(season)
+ self.season: int = int(season)
else:
- self.season = int(self.config.get("Settings", "season"))
+ self.season: int = settings.season
self.save_data = save_data
# refresh data pulled from external web sources: bad boy data from USA Today, beef data from Sleeper API
@@ -103,7 +102,6 @@ def __init__(self,
# retrieve all league data from respective platform API
self.league: BaseLeague = league_data_factory(
- config=self.config,
base_dir=base_dir,
data_dir=self.data_dir,
platform=self.platform,
@@ -128,7 +126,7 @@ def __init__(self,
else:
self.playoff_probs = None
- if self.config.getboolean("Report", "league_bad_boy_rankings"):
+ if settings.report_settings.league_bad_boy_rankings_bool:
begin = datetime.datetime.now()
logger.info(
f"Retrieving bad boy data from https://www.usatoday.com/sports/nfl/arrests/ "
@@ -143,7 +141,7 @@ def __init__(self,
else:
self.bad_boy_stats = None
- if self.config.getboolean("Report", "league_beef_rankings"):
+ if settings.report_settings.league_beef_rankings_bool:
begin = datetime.datetime.now()
logger.info(
f"Retrieving beef data from Sleeper "
@@ -158,21 +156,6 @@ def __init__(self,
else:
self.beef_stats = None
- if self.config.getboolean("Report", "league_covid_risk_rankings") and int(self.season) >= 2020:
- begin = datetime.datetime.now()
- logger.info(
- f"Retrieving COVID-19 risk data from https://sportsdata.usatoday.com/football/nfl/transactions "
- f"{'website' if not self.offline or self.refresh_web_data else 'saved data'}..."
- )
- self.covid_risk = self.league.get_covid_risk(self.save_data, self.offline, self.refresh_web_data)
- delta = datetime.datetime.now() - begin
- logger.info(
- f"...retrieved all COVID-19 risk data from https://sportsdata.usatoday.com/football/nfl/transactions "
- f"{'website' if not self.offline else 'saved data'} in {delta}\n"
- )
- else:
- self.covid_risk = None
-
# output league info for verification
logger.info(
f"...setup complete for "
@@ -204,13 +187,11 @@ def create_pdf_report(self) -> str:
while week_counter <= self.league.week_for_report:
week_for_report = self.league.week_for_report
- metrics_calculator = CalculateMetrics(self.config, self.league_id, self.league.num_playoff_slots,
- self.playoff_prob_sims)
+ metrics_calculator = CalculateMetrics(self.league_id, self.league.num_playoff_slots, self.playoff_prob_sims)
custom_weekly_matchups = self.league.get_custom_weekly_matchups(week_counter)
report_data = ReportData(
- config=self.config,
league=self.league,
season_weekly_teams_results=season_weekly_teams_results,
week_counter=week_counter,
@@ -218,7 +199,7 @@ def create_pdf_report(self) -> str:
season=self.season,
metrics_calculator=metrics_calculator,
metrics={
- "coaching_efficiency": CoachingEfficiency(self.config, self.league),
+ "coaching_efficiency": CoachingEfficiency(self.league),
"luck": metrics_calculator.calculate_luck(
week_counter,
self.league,
@@ -231,8 +212,7 @@ def create_pdf_report(self) -> str:
),
"playoff_probs": self.playoff_probs,
"bad_boy_stats": self.bad_boy_stats,
- "beef_stats": self.beef_stats,
- "covid_risk": self.covid_risk
+ "beef_stats": self.beef_stats
},
break_ties=self.break_ties,
dq_ce=self.dq_ce,
@@ -380,14 +360,14 @@ def create_pdf_report(self) -> str:
filename = self.league.name.replace(" ", "-") + "(" + str(self.league_id) + ")_week-" + str(
self.league.week_for_report) + "_report.pdf"
report_save_dir = (
- Path(self.config.get("Configuration", "output_dir")) / str(self.league.season) / f"{self.league.name.replace(' ', '-')}({self.league_id})"
+ settings.output_dir_local_path / str(self.league.season)
+ / f"{self.league.name.replace(' ', '-')}({self.league_id})"
)
- report_title_text = \
- self.league.name + " (" + str(self.league_id) + ") Week " + \
- str(self.league.week_for_report) + " Report"
+ report_title_text = f"{self.league.name} ({self.league_id}) Week {self.league.week_for_report} Report"
report_footer_text = (
f""
- f"Report generated {datetime.datetime.now():%Y-%b-%d %H:%M:%S} for {self.platform_display} Fantasy Football league \"{self.league.name}\" with id {self.league_id} "
+ f"Report generated {datetime.datetime.now():%Y-%b-%d %H:%M:%S} for {self.platform_display} "
+ f"Fantasy Football league \"{self.league.name}\" with id {self.league_id} "
f"({self.league.url})."
f"
"
f"If you enjoy using the Fantasy Football Metrics Weekly Report app, please feel free help support its "
@@ -401,11 +381,10 @@ def create_pdf_report(self) -> str:
if not self.test:
filename_with_path = Path(report_save_dir) / filename
else:
- filename_with_path = Path(self.config.get("Configuration", "output_dir")) / "test_report.pdf"
+ filename_with_path = settings.output_dir_local_path / "test_report.pdf"
# instantiate pdf generator
pdf_generator = PdfGenerator(
- config=self.config,
season=self.season,
league=self.league,
playoff_prob_sims=self.playoff_prob_sims,
diff --git a/report/constants.py b/report/constants.py
deleted file mode 100644
index 0ceb6705..00000000
--- a/report/constants.py
+++ /dev/null
@@ -1,17 +0,0 @@
-
-from typing import List, Dict
-
-# nfl team abbreviations
-nfl_team_abbreviations: List[str] = [
- "ARI", "ATL", "BAL", "BUF", "CAR", "CHI", "CIN", "CLE",
- "DAL", "DEN", "DET", "GB", "HOU", "IND", "JAX", "KC",
- "LAR", "LAC", "LV", "MIA", "MIN", "NE", "NO", "NYG",
- "NYJ", "PHI", "PIT", "SEA", "SF", "TB", "TEN", "WAS"
-]
-
-# small reference dict to convert between commonly used alternate team abbreviations
-nfl_team_abbreviation_conversions: Dict[str, str] = {
- "JAC": "JAX",
- "LA": "LAR",
- "WSH": "WAS"
-}
diff --git a/report/data.py b/report/data.py
index 8aeae2cf..3c59ab96 100644
--- a/report/data.py
+++ b/report/data.py
@@ -7,16 +7,16 @@
from calculate.metrics import CalculateMetrics
from calculate.points_by_position import PointsByPosition
from dao.base import BaseLeague, BaseMatchup, BaseTeam
-from report.logger import get_logger
from utilities.app import add_report_team_stats, get_player_game_time_statuses
-from utilities.config import AppConfigParser
+from utilities.logger import get_logger
+from utilities.settings import settings
logger = get_logger(__name__, propagate=False)
class ReportData(object):
- def __init__(self, config: AppConfigParser, league: BaseLeague, season_weekly_teams_results, week_counter: int,
+ def __init__(self, league: BaseLeague, season_weekly_teams_results, week_counter: int,
week_for_report: int, season: int, metrics_calculator: CalculateMetrics, metrics,
break_ties: bool = False, dq_ce: bool = False, testing: bool = False):
logger.debug("Instantiating report data.")
@@ -43,11 +43,9 @@ def __init__(self, config: AppConfigParser, league: BaseLeague, season_weekly_te
self.teams_results = {
team.team_id: add_report_team_stats(
- config,
team,
league,
week_counter,
- season,
metrics_calculator,
metrics,
dq_ce,
@@ -67,16 +65,14 @@ def __init__(self, config: AppConfigParser, league: BaseLeague, season_weekly_te
)
)
- # option to disqualify manually configured team(s) (in config.ini) for current week of coaching efficiency
+ # option to disqualify team(s) manually entered in the .env file for current week of coaching efficiency
self.coaching_efficiency_dqs = {}
if week_counter == week_for_report:
- disqualified_teams = config.get("Settings", "coaching_efficiency_disqualified_teams")
- if disqualified_teams:
- for team in disqualified_teams.split(","):
- self.coaching_efficiency_dqs[team] = -2
- for team_result in self.teams_results.values():
- if team == team_result.name:
- team_result.coaching_efficiency = "DQ"
+ for team in settings.coaching_efficiency_disqualified_teams:
+ self.coaching_efficiency_dqs[team] = -2
+ for team_result in self.teams_results.values():
+ if team == team_result.name:
+ team_result.coaching_efficiency = "DQ"
# used only for testing what happens when different metrics are tied; requires uncommenting lines in method
if testing:
@@ -216,10 +212,6 @@ def __init__(self, config: AppConfigParser, league: BaseLeague, season_weekly_te
self.data_for_beef_rankings = metrics_calculator.get_beef_rank_data(
sorted(self.teams_results.values(), key=lambda x: x.tabbu, reverse=True))
- # covid risk data
- self.data_for_covid_risk_rankings = metrics_calculator.get_covid_risk_rank_data(
- sorted(self.teams_results.values(), key=lambda x: str(x.name).lower(), reverse=True))
-
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ COUNT METRIC TIES ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
diff --git a/report/pdf/charts/bar.py b/report/pdf/charts/bar.py
index 1169c56f..18ad5529 100644
--- a/report/pdf/charts/bar.py
+++ b/report/pdf/charts/bar.py
@@ -4,15 +4,15 @@
# code snippets: http://www.reportlab.com/chartgallery/
import json
+from typing import List, Any
from reportlab.graphics.charts.barcharts import HorizontalBarChart3D
from reportlab.graphics.charts.textlabels import Label
# noinspection PyProtectedMember
from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin
from reportlab.lib.colors import PCMYKColor
-from typing import List, Any
-from report.logger import get_logger
+from utilities.logger import get_logger
logger = get_logger(__name__, propagate=False)
diff --git a/report/pdf/charts/line.py b/report/pdf/charts/line.py
index 9758bf06..c1b3729b 100644
--- a/report/pdf/charts/line.py
+++ b/report/pdf/charts/line.py
@@ -16,7 +16,7 @@
from reportlab.lib.colors import PCMYKColor, black
from reportlab.lib.validators import Auto
-from report.logger import get_logger
+from utilities.logger import get_logger
logger = get_logger(__name__, propagate=False)
diff --git a/report/pdf/charts/pie.py b/report/pdf/charts/pie.py
index db26408b..f85ea0ba 100644
--- a/report/pdf/charts/pie.py
+++ b/report/pdf/charts/pie.py
@@ -13,7 +13,7 @@
from reportlab.lib.colors import HexColor, black
from reportlab.lib.colors import white
-from report.logger import get_logger
+from utilities.logger import get_logger
logger = get_logger(__name__, propagate=False)
diff --git a/report/pdf/generator.py b/report/pdf/generator.py
index 094d5689..32acb323 100644
--- a/report/pdf/generator.py
+++ b/report/pdf/generator.py
@@ -30,12 +30,12 @@
from dao.base import BaseLeague, BaseTeam, BasePlayer
from report.data import ReportData
-from report.logger import get_logger
from report.pdf.charts.bar import HorizontalBarChart3DGenerator
from report.pdf.charts.line import LineChartGenerator
from report.pdf.charts.pie import BreakdownPieDrawing
from resources.documentation import descriptions
-from utilities.config import AppConfigParser
+from utilities.logger import get_logger
+from utilities.settings import settings
from utilities.utils import truncate_cell_for_display
logger = get_logger(__name__, propagate=False)
@@ -135,12 +135,11 @@ def drawOn(self, canvas: Canvas, x: int, y: int, _sW: int = 0):
class PdfGenerator(object):
# noinspection SpellCheckingInspection
- def __init__(self, config: AppConfigParser, season: int, league: BaseLeague, playoff_prob_sims: int,
+ def __init__(self, season: int, league: BaseLeague, playoff_prob_sims: int,
report_title_text: str, report_footer_text: str, report_data: ReportData):
logger.debug("Instantiating PDF generator.")
- # report configuration
- self.config = config
+ # report settings
self.season = season
self.league_id = league.league_id
self.playoff_slots = int(league.num_playoff_slots)
@@ -162,35 +161,46 @@ def __init__(self, config: AppConfigParser, season: int, league: BaseLeague, pla
self.widths_05_cols_no_1 = [0.45 * inch, 1.95 * inch, 1.85 * inch, 1.75 * inch, 1.75 * inch] # 7.75
# ..........................Place........Team.........Manager......Col 4........Col 5........Col 6.....
- self.widths_06_cols_no_1 = [0.45 * inch, 1.95 * inch, 1.85 * inch, 1.00 * inch, 1.50 * inch, 1.00 * inch] # 7.75
+ self.widths_06_cols_no_1 = [0.45 * inch, 1.95 * inch, 1.85 * inch, 1.00 * inch, 1.50 * inch,
+ 1.00 * inch] # 7.75
# ..........................Place........Team.........Manager......Col 4........Col 5........Col 6.....
- self.widths_06_cols_no_2 = [0.45 * inch, 1.95 * inch, 1.35 * inch, 1.00 * inch, 2.00 * inch, 1.00 * inch] # 7.75
+ self.widths_06_cols_no_2 = [0.45 * inch, 1.95 * inch, 1.35 * inch, 1.00 * inch, 2.00 * inch,
+ 1.00 * inch] # 7.75
# ..........................Place........Team.........Manager......Col 4........Col 5........Col 6.....
- self.widths_06_cols_no_3 = [0.45 * inch, 1.95 * inch, 1.85 * inch, 0.60 * inch, 1.45 * inch, 1.45 * inch] # 7.75
+ self.widths_06_cols_no_3 = [0.45 * inch, 1.95 * inch, 1.85 * inch, 0.60 * inch, 1.45 * inch,
+ 1.45 * inch] # 7.75
# ..........................Place........Team.........Manager......Col 4........Col 5........Col 6........Col 7.....
- self.widths_07_cols_no_1 = [0.45 * inch, 1.80 * inch, 1.50 * inch, 0.75 * inch, 1.50 * inch, 0.75 * inch, 1.00 * inch] # 7.75
+ self.widths_07_cols_no_1 = [0.45 * inch, 1.80 * inch, 1.50 * inch, 0.75 * inch, 1.50 * inch, 0.75 * inch,
+ 1.00 * inch] # 7.75
# ..........................Place........Team.........Manager......Col 4........Col 5........Col 6........Col 7.....
- self.widths_07_cols_no_2 = [0.45 * inch, 1.80 * inch, 1.50 * inch, 1.10 * inch, 1.00 * inch, 1.05 * inch, 0.85 * inch] # 7.75
+ self.widths_07_cols_no_2 = [0.45 * inch, 1.80 * inch, 1.50 * inch, 1.10 * inch, 1.00 * inch, 1.05 * inch,
+ 0.85 * inch] # 7.75
# ..........................Place........Team.........Manager......Record.......Pts For......Pts Against..Streak.......Waiver.......Moves........Trades....
- self.widths_10_cols_no_1 = [0.45 * inch, 1.80 * inch, 1.10 * inch, 1.00 * inch, 0.80 * inch, 1.05 * inch, 0.50 * inch, 0.50 * inch, 0.50 * inch, 0.50 * inch] # 8.20
+ self.widths_10_cols_no_1 = [0.45 * inch, 1.80 * inch, 1.10 * inch, 1.00 * inch, 0.80 * inch, 1.05 * inch,
+ 0.50 * inch, 0.50 * inch, 0.50 * inch, 0.50 * inch] # 8.20
# ..........................Place........Team.........Manager......Record.......Div Rec......Pts For......Pts Against..Streak.......Waiver.......Moves........Trades....
- self.widths_11_cols_no_1 = [0.40 * inch, 1.65 * inch, 0.80 * inch, 0.85 * inch, 0.85 * inch, 0.80 * inch, 0.90 * inch, 0.45 * inch, 0.50 * inch, 0.50 * inch, 0.50 * inch] # 8.20
+ self.widths_11_cols_no_1 = [0.40 * inch, 1.65 * inch, 0.80 * inch, 0.85 * inch, 0.85 * inch, 0.80 * inch,
+ 0.90 * inch, 0.45 * inch, 0.50 * inch, 0.50 * inch, 0.50 * inch] # 8.20
# ..........................Place........Team.........Manager......Record.......Pts For......Pts Against..Streak.......Waiver.......FAAB.........Moves........Trades....
- self.widths_11_cols_no_2 = [0.40 * inch, 1.65 * inch, 0.90 * inch, 1.00 * inch, 0.80 * inch, 1.05 * inch, 0.45 * inch, 0.50 * inch, 0.45 * inch, 0.50 * inch, 0.50 * inch] # 8.20
+ self.widths_11_cols_no_2 = [0.40 * inch, 1.65 * inch, 0.90 * inch, 1.00 * inch, 0.80 * inch, 1.05 * inch,
+ 0.45 * inch, 0.50 * inch, 0.45 * inch, 0.50 * inch, 0.50 * inch] # 8.20
# ..........................Place........Team.........Manager......Record.......Div Rec......Pts For......Pts Against..Streak.......Waiver.......FAAB.........Moves........Trades...
- self.widths_12_cols_no_1 = [0.40 * inch, 1.55 * inch, 0.85 * inch, 0.85 * inch, 0.85 * inch, 0.65 * inch, 0.65 * inch, 0.45 * inch, 0.50 * inch, 0.45 * inch, 0.50 * inch, 0.50 * inch] # 8.20
+ self.widths_12_cols_no_1 = [0.40 * inch, 1.55 * inch, 0.85 * inch, 0.85 * inch, 0.85 * inch, 0.65 * inch,
+ 0.65 * inch, 0.45 * inch, 0.50 * inch, 0.45 * inch, 0.50 * inch,
+ 0.50 * inch] # 8.20
if self.playoff_slots > 0:
# .........................Team.........Manager......Record.......Pts For......Pts Against....Finish Positions..............................................
- self.widths_n_cols_no_1 = [1.55 * inch, 1.00 * inch, 0.95 * inch, 0.65 * inch, 0.65 * inch] + [round(3.4 / self.playoff_slots, 2) * inch] * self.playoff_slots # 8.20
+ self.widths_n_cols_no_1 = [1.55 * inch, 1.00 * inch, 0.95 * inch, 0.65 * inch, 0.65 * inch] + [
+ round(3.4 / self.playoff_slots, 2) * inch] * self.playoff_slots # 8.20
self.line_separator = Drawing(100, 1)
self.line_separator.add(Line(0, -65, 550, -65, strokeColor=colors.black, strokeWidth=1))
@@ -201,10 +211,10 @@ def __init__(self, config: AppConfigParser, season: int, league: BaseLeague, pla
self.spacer_three_inch = Spacer(1, 3.00 * inch)
self.spacer_five_inch = Spacer(1, 5.00 * inch)
- # configure text styles
- self.font_size = config.getint("Report", "font_size", fallback=12)
- font_key = config.get("Report", "font", fallback="helvetica")
- supported_fonts = [font.strip() for font in self.config.get("Report", "supported_fonts").split(",")]
+ # set text styles
+ self.font_size = settings.report_settings.font_size
+ font_key = settings.report_settings.font
+ supported_fonts = settings.report_settings.supported_fonts
if font_key not in supported_fonts:
logger.warning(
f"The {font_key} font is not supported at this time. Report formatting has defaulted to Helvetica. "
@@ -234,7 +244,7 @@ def __init__(self, config: AppConfigParser, season: int, league: BaseLeague, pla
self.font_bold = "Times-Bold"
self.font_italic = "Times-Italic"
self.font_bold_italic = "Times-BoldItalic"
- # configure custom font(s)
+ # set custom font(s)
elif use_custom_font and which_font == 3:
self.font = "Symbola"
self.font_bold = "Symbola"
@@ -321,7 +331,7 @@ def __init__(self, config: AppConfigParser, season: int, league: BaseLeague, pla
self.text_style_small = ParagraphStyle(name="small", fontSize=5, alignment=TA_CENTER)
self.text_style_invisible = ParagraphStyle(name="invisible", fontSize=0, textColor=colors.white)
- # configure word wrap
+ # set word wrap
self.text_style.wordWrap = "CJK"
title_table_style_list = [
@@ -393,7 +403,8 @@ def __init__(self, config: AppConfigParser, season: int, league: BaseLeague, pla
# report specific document elements
self.standings_headers = [
- ["Place", "Team", "Manager", "Record", "Points For", "Points Against", "Streak", "Waiver", "Moves", "Trades"]
+ ["Place", "Team", "Manager", "Record", "Points For", "Points Against", "Streak", "Waiver", "Moves",
+ "Trades"]
]
self.median_standings_headers = [
["Place", "Team", "Manager", "Combined Record", "Median Record", "Season +/- Median", "Median Streak"]
@@ -494,7 +505,6 @@ def __init__(self, config: AppConfigParser, season: int, league: BaseLeague, pla
self.data_for_z_scores = report_data.data_for_z_scores
self.data_for_bad_boy_rankings = report_data.data_for_bad_boy_rankings
self.data_for_beef_rankings = report_data.data_for_beef_rankings
- self.data_for_covid_risk_rankings = report_data.data_for_covid_risk_rankings
self.data_for_weekly_points_by_position = report_data.data_for_weekly_points_by_position
self.data_for_season_average_team_points_by_position = report_data.data_for_season_avg_points_by_position
self.data_for_season_weekly_top_scorers = report_data.data_for_season_weekly_top_scorers
@@ -513,14 +523,14 @@ def __init__(self, config: AppConfigParser, season: int, league: BaseLeague, pla
self.report_data.ties_for_power_rankings, table_style_list, "power_ranking"
)
self.style_tied_bad_boy = self.set_tied_values_style(
- self.report_data.ties_for_power_rankings, table_style_list,"bad_boy"
+ self.report_data.ties_for_power_rankings, table_style_list, "bad_boy"
)
self.style_tied_beef = self.set_tied_values_style(
self.report_data.ties_for_beef_rankings, style_left_alight_right_col_list, "beef"
)
# table of contents
- self.toc = TableOfContents(self.font, self.font_size, self.config, self.break_ties)
+ self.toc = TableOfContents(self.font, self.font_size, self.break_ties)
# appendix
self.appendix = Appendix(
@@ -879,7 +889,7 @@ def create_data_table(self, metric_type: str, col_headers: List[List[str]], data
if isinstance(cell, str):
# truncate data cell contents to specified max characters
display_row.append(
- truncate_cell_for_display(self.config, cell, halve_max_chars=(cell_ndx == manager_header_ndx))
+ truncate_cell_for_display(cell, halve_max_chars=(cell_ndx == manager_header_ndx))
)
else:
display_row.append(cell)
@@ -908,7 +918,7 @@ def create_line_chart(self, data: List[Any], data_length: int, series_names: Lis
display_series_names = []
for name in series_names:
# truncate series name to specified max characters
- display_series_names.append(truncate_cell_for_display(self.config, str(name)))
+ display_series_names.append(truncate_cell_for_display(str(name)))
series_names = display_series_names
@@ -952,7 +962,6 @@ def create_line_chart(self, data: List[Any], data_length: int, series_names: Lis
k_colors.add(color[3])
while additional_team_count > 0:
-
c_color = choice(list(set(list(range(0, 101))) - c_colors))
m_color = choice(list(set(list(range(0, 101))) - m_colors))
y_color = choice(list(set(list(range(0, 101))) - y_colors))
@@ -1008,7 +1017,7 @@ def create_3d_horizontal_bar_chart(self, data: List[List[Any]], x_axis_title: st
chart_width = 425
chart_height = 425
- covid_risk_bar_chart = HorizontalBarChart3DGenerator(
+ horizontal_bar_chart = HorizontalBarChart3DGenerator(
data,
self.font,
self.font_size,
@@ -1019,7 +1028,7 @@ def create_3d_horizontal_bar_chart(self, data: List[List[Any]], x_axis_title: st
chart_height
)
- return covid_risk_bar_chart
+ return horizontal_bar_chart
@staticmethod
def get_img(path: Union[Path, str], width: int = 1 * inch, hyperlink: str = None) -> ReportLabImage:
@@ -1056,18 +1065,17 @@ def create_team_stats_pages(self, doc_elements: List[Flowable], weekly_team_data
team_result: BaseTeam = self.teams_results[team_id]
player_info = team_result.roster
- if self.config.getboolean(
- "Report", "team_points_by_position_charts") or self.config.getboolean(
- "Report", "team_bad_boy_stats") or self.config.getboolean(
- "Report", "team_beef_stats") or self.config.getboolean(
- "Report", "team_boom_or_bust"):
+ if (settings.report_settings.team_points_by_position_charts_bool
+ or settings.report_settings.team_bad_boy_stats_bool
+ or settings.report_settings.team_beef_stats_bool
+ or settings.report_settings.team_boom_or_bust_bool):
title = self.create_title("" + team_result.name + "", element_type="section",
anchor="")
self.toc.add_team_section(team_result.name)
doc_elements.append(title)
- if self.config.getboolean("Report", "team_points_by_position_charts"):
+ if settings.report_settings.team_points_by_position_charts_bool:
labels = []
weekly_data = []
season_data = [x[1] for x in season_average_team_data_by_position.get(team_id)]
@@ -1090,8 +1098,8 @@ def create_team_stats_pages(self, doc_elements: List[Flowable], weekly_team_data
doc_elements.append(KeepTogether(team_table))
doc_elements.append(self.spacer_quarter_inch)
- if (self.config.getboolean("Report", "league_bad_boy_rankings")
- and self.config.getboolean("Report", "team_bad_boy_stats")):
+ if (settings.report_settings.league_bad_boy_rankings_bool
+ and settings.report_settings.team_bad_boy_stats_bool):
if player_info:
offending_players = []
@@ -1118,8 +1126,8 @@ def create_team_stats_pages(self, doc_elements: List[Flowable], weekly_team_data
doc_elements.append(KeepTogether(bad_boys_table))
doc_elements.append(self.spacer_tenth_inch)
- if (self.config.getboolean("Report", "league_beef_rankings")
- and self.config.getboolean("Report", "team_beef_stats")):
+ if (settings.report_settings.league_beef_rankings_bool
+ and settings.report_settings.team_beef_stats_bool):
if player_info:
doc_elements.append(self.create_title("Beefiest Bois", 8.5, "section"))
@@ -1149,7 +1157,7 @@ def create_team_stats_pages(self, doc_elements: List[Flowable], weekly_team_data
doc_elements.append(KeepTogether(beefy_boi_table))
doc_elements.append(self.spacer_tenth_inch)
- if self.config.getboolean("Report", "team_boom_or_bust"):
+ if settings.report_settings.team_boom_or_bust_bool:
if player_info:
starting_players = []
player: BasePlayer
@@ -1192,12 +1200,12 @@ def create_team_stats_pages(self, doc_elements: List[Flowable], weekly_team_data
best_player_headshot = get_player_image(
best_weekly_player.headshot_url, self.data_dir, self.week_for_report,
- self.config.getint("Report", "image_quality"), 1.5 * inch, best_weekly_player.full_name,
+ settings.report_settings.image_quality, 1.5 * inch, best_weekly_player.full_name,
self.report_data.league.offline
)
worst_player_headshot = get_player_image(
worst_weekly_player.headshot_url, self.data_dir, self.week_for_report,
- self.config.getint("Report", "image_quality"), 1.5 * inch, worst_weekly_player.full_name,
+ settings.report_settings.image_quality, 1.5 * inch, worst_weekly_player.full_name,
self.report_data.league.offline
)
@@ -1268,11 +1276,10 @@ def create_team_stats_pages(self, doc_elements: List[Flowable], weekly_team_data
doc_elements.append(self.spacer_tenth_inch)
doc_elements.append(KeepTogether(table))
- if self.config.getboolean(
- "Report", "team_points_by_position_charts") or self.config.getboolean(
- "Report", "team_bad_boy_stats") or self.config.getboolean(
- "Report", "team_beef_stats") or self.config.getboolean(
- "Report", "team_boom_or_bust"):
+ if (settings.report_settings.team_points_by_position_charts_bool
+ or settings.report_settings.team_bad_boy_stats_bool
+ or settings.report_settings.team_beef_stats_bool
+ or settings.report_settings.team_boom_or_bust_bool):
doc_elements.append(self.add_page_break())
def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List[Any]]) -> str:
@@ -1302,7 +1309,7 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List
elements.append(self.add_page_break())
# standings
- if self.config.getboolean("Report", "league_standings"):
+ if settings.report_settings.league_standings_bool:
# update standings style to vertically justify all rows
standings_style = deepcopy(self.style)
standings_style.add("VALIGN", (0, 0), (-1, -1), "MIDDLE")
@@ -1364,7 +1371,7 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List
elements.append(standings)
elements.append(self.spacer_tenth_inch)
- if self.config.getboolean("Report", "league_playoff_probs") and self.playoff_slots > 0:
+ if settings.report_settings.league_playoff_probs_bool and self.playoff_slots > 0:
# update playoff probabilities style to make playoff teams green
playoff_probs_style = deepcopy(self.style)
playoff_probs_style.add("TEXTCOLOR", (0, 1), (-1, self.playoff_slots), colors.green)
@@ -1373,8 +1380,8 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List
self.playoff_probs_headers[0].insert(3, "Division")
playoff_probs_style.add("FONTSIZE", (0, 0), (-1, -1), self.font_size - 4)
self.widths_n_cols_no_1 = (
- [1.35 * inch, 0.90 * inch, 0.75 * inch, 0.75 * inch, 0.50 * inch, 0.50 * inch] +
- [round(3.4 / self.playoff_slots, 2) * inch] * self.playoff_slots
+ [1.35 * inch, 0.90 * inch, 0.75 * inch, 0.75 * inch, 0.50 * inch, 0.50 * inch] +
+ [round(3.4 / self.playoff_slots, 2) * inch] * self.playoff_slots
)
data_for_playoff_probs = self.report_data.data_for_playoff_probs
@@ -1404,8 +1411,9 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List
# playoff probabilities
if data_for_playoff_probs:
num_playoff_simulations = (
- int(self.playoff_prob_sims) if self.playoff_prob_sims is not None
- else self.config.getint("Settings", "num_playoff_simulations", fallback=100000)
+ int(self.playoff_prob_sims)
+ if self.playoff_prob_sims is not None
+ else settings.num_playoff_simulations
)
elements.append(self.create_section(
@@ -1416,28 +1424,28 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List
playoff_probs_style,
self.widths_n_cols_no_1,
subtitle_text=(
- f"Playoff probabilities were calculated using {num_playoff_simulations:,} Monte Carlo "
- f"simulations to predict team performances through the end of the regular fantasy season."
- + ("\nProbabilities account for division winners in addition to overall win/loss/tie record."
- if self.report_data.has_divisions else "")
+ f"Playoff probabilities were calculated using {num_playoff_simulations:,} Monte Carlo "
+ f"simulations to predict team performances through the end of the regular fantasy season."
+ + (
+ "\nProbabilities account for division winners in addition to overall win/loss/tie record."
+ if self.report_data.has_divisions else "")
),
metric_type="playoffs",
footer_text=(
f"""† Predicted Division Leaders{
- "
"
- "‡ Predicted Division Qualifiers"
- if self.config.getint("Settings", "num_playoff_slots_per_division", fallback=1) > 1 else ""
+ "
"
+ "‡ Predicted Division Qualifiers"
+ if settings.num_playoff_slots_per_division > 1
+ else ""
}"""
if self.report_data.has_divisions else None
)
))
- if (self.config.getboolean("Report", "league_standings")
- or self.config.getboolean("Report", "league_playoff_probs")):
-
+ if settings.report_settings.league_standings_bool or settings.report_settings.league_playoff_probs_bool:
elements.append(self.add_page_break())
- if self.config.getboolean("Report", "league_median_standings"):
+ if settings.report_settings.league_median_standings_bool:
# update median standings style to italicize ranking column
median_standings_style = deepcopy(self.style)
median_standings_style.add("FONT", (3, 1), (3, -1), self.font_italic)
@@ -1458,11 +1466,9 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List
)
))
elements.append(self.spacer_twentieth_inch)
-
- if self.config.getboolean("Report", "league_median_standings"):
elements.append(self.add_page_break())
- if self.config.getboolean("Report", "league_power_rankings"):
+ if settings.report_settings.league_power_rankings_bool:
# power ranking
elements.append(self.create_section(
"Team Power Rankings",
@@ -1477,7 +1483,7 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List
))
elements.append(self.spacer_twentieth_inch)
- if self.config.getboolean("Report", "league_z_score_rankings"):
+ if settings.report_settings.league_z_score_rankings_bool:
# z-scores (if week 3 or later, once meaningful z-scores can be calculated)
if self.data_for_z_scores:
elements.append(self.create_section(
@@ -1496,11 +1502,10 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List
]
))
- if self.config.getboolean("Report", "league_power_rankings") or self.config.getboolean(
- "Report", "league_z_score_rankings"):
+ if settings.report_settings.league_power_rankings_bool or settings.report_settings.league_z_score_rankings_bool:
elements.append(self.add_page_break())
- if self.config.getboolean("Report", "league_score_rankings"):
+ if settings.report_settings.league_score_rankings_bool:
# scores
elements.append(self.create_section(
"Team Score Rankings",
@@ -1514,7 +1519,7 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List
))
elements.append(self.spacer_twentieth_inch)
- if self.config.getboolean("Report", "league_coaching_efficiency_rankings"):
+ if settings.report_settings.league_coaching_efficiency_rankings_bool:
# coaching efficiency
elements.append(self.create_section(
"Team Coaching Efficiency Rankings",
@@ -1529,7 +1534,7 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List
))
elements.append(self.spacer_twentieth_inch)
- if self.config.getboolean("Report", "league_luck_rankings"):
+ if settings.report_settings.league_luck_rankings_bool:
# luck
elements.append(self.create_section(
"Team Luck Rankings",
@@ -1543,12 +1548,12 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List
metric_type="luck"
))
- if self.config.getboolean("Report", "league_score_rankings") or self.config.getboolean(
- "Report", "league_coaching_efficiency_rankings") or self.config.getboolean("Report",
- "league_luck_rankings"):
+ if (settings.report_settings.league_score_rankings_bool
+ or settings.report_settings.league_coaching_efficiency_rankings_bool
+ or settings.report_settings.league_luck_rankings_bool):
elements.append(self.add_page_break())
- if self.config.getboolean("Report", "league_optimal_score_rankings"):
+ if settings.report_settings.league_optimal_score_rankings_bool:
# optimal scores
elements.append(self.create_section(
"Team Optimal Score Rankings",
@@ -1559,11 +1564,9 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List
self.widths_05_cols_no_1
))
elements.append(self.spacer_twentieth_inch)
-
- if self.config.getboolean("Report", "league_optimal_score_rankings"):
elements.append(self.add_page_break())
- if self.config.getboolean("Report", "league_bad_boy_rankings"):
+ if settings.report_settings.league_bad_boy_rankings_bool:
# bad boy rankings
elements.append(self.create_section(
"Bad Boy Rankings",
@@ -1577,7 +1580,7 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List
))
elements.append(self.spacer_twentieth_inch)
- if self.config.getboolean("Report", "league_beef_rankings"):
+ if settings.report_settings.league_beef_rankings_bool:
# beef rankings
elements.append(self.create_section(
"Beef Rankings",
@@ -1596,11 +1599,10 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List
]
))
- if self.config.getboolean("Report", "league_bad_boy_rankings") or self.config.getboolean(
- "Report", "league_beef_rankings"):
+ if settings.report_settings.league_bad_boy_rankings_bool or settings.report_settings.league_beef_rankings_bool:
elements.append(self.add_page_break())
- if self.config.getboolean("Report", "league_weekly_top_scorers"):
+ if settings.report_settings.league_weekly_top_scorers_bool:
weekly_top_scorers_title_str = "Weekly Top Scorers"
weekly_top_scorers_page_title = self.create_title(
"" + weekly_top_scorers_title_str + "", element_type="chart",
@@ -1620,7 +1622,7 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List
))
elements.append(self.spacer_twentieth_inch)
- if self.config.getboolean("Report", "league_weekly_highest_ce"):
+ if settings.report_settings.league_weekly_highest_ce_bool:
weekly_highest_ce_title_str = "Weekly Highest Coaching Efficiency"
weekly_highest_ce_page_title = self.create_title(
"" + weekly_highest_ce_title_str + "", element_type="chart",
@@ -1639,42 +1641,11 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List
section_title_function=self.toc.add_top_performers_section
))
- if self.config.getboolean("Report", "league_weekly_top_scorers") or self.config.getboolean(
- "Report", "league_weekly_highest_ce"):
- elements.append(self.add_page_break())
-
- if self.config.getboolean("Report", "league_covid_risk_rankings") and int(self.season) >= 2020:
- # covid risk rankings
-
- # create bar chart for covid risk
- charts_covid_page_title_str = "COVID-19 Risk"
- section_anchor = str(self.toc.get_current_anchor())
- self.appendix.add_entry(
- charts_covid_page_title_str,
- section_anchor,
- getattr(descriptions, charts_covid_page_title_str.replace(" ", "_").replace("-", "_").lower())
- )
- appendix_anchor = self.appendix.get_last_entry_anchor()
- title = self.create_title(
- '''''' +
- '''''' + charts_covid_page_title_str + '''''',
- element_type="section",
- anchor="",
- subtitle_text=[
- "Team COVID-19 Risk is calculated based on the cumulative COVID-19 risk of all players on "
- "that team's starting lineup. Each player is evaluated against both their own and their teammates' "
- "appearances on the NFL's Reserve/COVID-19 list and how recently those appearances occurred."
- ]
- )
- self.toc.add_chart_section(charts_covid_page_title_str)
-
- elements.append(title)
- elements.append(KeepTogether(self.create_3d_horizontal_bar_chart(self.data_for_covid_risk_rankings,
- "Risk Factor", 10)))
- elements.append(self.spacer_twentieth_inch)
+ if (settings.report_settings.league_weekly_top_scorers_bool
+ or settings.report_settings.league_weekly_highest_ce_bool):
elements.append(self.add_page_break())
- if self.config.getboolean("Report", "report_time_series_charts"):
+ if settings.report_settings.report_time_series_charts_bool:
series_names = line_chart_data_list[0]
points_data = line_chart_data_list[2]
efficiency_data = line_chart_data_list[3]
@@ -1715,12 +1686,11 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List
elements.append(self.spacer_tenth_inch)
elements.append(self.add_page_break())
- if self.config.getboolean(
- "Report", "report_team_stats") and (self.config.getboolean(
- "Report", "team_points_by_position_charts") or self.config.getboolean(
- "Report", "team_bad_boy_stats") or self.config.getboolean(
- "Report", "team_beef_stats") or self.config.getboolean(
- "Report", "team_boom_or_bust")):
+ if (settings.report_settings.report_team_stats_bool
+ and settings.report_settings.team_points_by_position_charts_bool
+ and settings.report_settings.team_bad_boy_stats_bool
+ and settings.report_settings.team_beef_stats_bool
+ and settings.report_settings.team_boom_or_bust_bool):
# dynamically build additional pages for individual team stats
self.create_team_stats_pages(
elements, self.data_for_weekly_points_by_position, self.data_for_season_average_team_points_by_position
@@ -1747,9 +1717,8 @@ def generate_pdf(self, filename_with_path: Path, line_chart_data_list: List[List
class TableOfContents(object):
- def __init__(self, font, font_size, config, break_ties):
+ def __init__(self, font, font_size, break_ties):
- self.config: AppConfigParser = config
self.break_ties = break_ties
self.toc_style_right = ParagraphStyle(name="tocr", alignment=TA_RIGHT, fontSize=font_size - 2, fontName=font)
@@ -1770,18 +1739,18 @@ def __init__(self, font, font_size, config, break_ties):
self.toc_chart_section_data = None
self.toc_team_section_data = None
self.toc_appendix_data = None
- if self.config.getboolean(
- "Report", "league_standings") or self.config.getboolean(
- "Report", "league_playoff_probs") or self.config.getboolean(
- "Report", "league_median_standings") or self.config.getboolean(
- "Report", "league_power_rankings") or self.config.getboolean(
- "Report", "league_z_score_rankings") or self.config.getboolean(
- "Report", "league_score_rankings") or self.config.getboolean(
- "Report", "league_coaching_efficiency_rankings") or self.config.getboolean(
- "Report", "league_luck_rankings") or self.config.getboolean(
- "Report", "league_optimal_score_rankings") or self.config.getboolean(
- "Report", "league_bad_boy_rankings") or self.config.getboolean(
- "Report", "league_beef_rankings"):
+
+ if (settings.report_settings.league_standings_bool
+ or settings.report_settings.league_playoff_probs_bool
+ or settings.report_settings.league_median_standings_bool
+ or settings.report_settings.league_power_rankings_bool
+ or settings.report_settings.league_z_score_rankings_bool
+ or settings.report_settings.league_score_rankings_bool
+ or settings.report_settings.league_coaching_efficiency_rankings_bool
+ or settings.report_settings.league_luck_rankings_bool
+ or settings.report_settings.league_optimal_score_rankings_bool
+ or settings.report_settings.league_bad_boy_rankings_bool
+ or settings.report_settings.league_beef_rankings_bool):
self.toc_metric_section_data = [
[Paragraph("Metrics", self.toc_style_title_right),
"",
@@ -1793,28 +1762,28 @@ def __init__(self, font, font_size, config, break_ties):
Paragraph("Page", self.toc_style_title_left)]
]
- if self.config.getboolean("Report", "league_weekly_top_scorers") or self.config.getboolean(
- "Report", "league_weekly_highest_ce"):
+ if (settings.report_settings.league_weekly_top_scorers_bool
+ or settings.report_settings.league_weekly_highest_ce_bool):
self.toc_top_performers_section_data = [
[Paragraph("Top Performers", self.toc_style_title_right),
"",
Paragraph("Page", self.toc_style_title_left)]
]
- if self.config.getboolean("Report", "report_time_series_charts") or self.config.getboolean(
- "Report", "league_covid_risk_rankings"):
+ if settings.report_settings.report_time_series_charts_bool:
self.toc_chart_section_data = [
[Paragraph("Charts", self.toc_style_title_right),
"",
Paragraph("Page", self.toc_style_title_left)]
]
- if self.config.getboolean(
- "Report", "report_team_stats") and (self.config.getboolean(
- "Report", "team_points_by_position_charts") or self.config.getboolean(
- "Report", "team_bad_boy_stats") or self.config.getboolean(
- "Report", "team_beef_stats") or self.config.getboolean(
- "Report", "team_boom_or_bust")):
+ if (settings.report_settings.report_team_stats_bool
+ and (
+ settings.report_settings.team_points_by_position_charts_bool
+ or settings.report_settings.team_bad_boy_stats_bool
+ or settings.report_settings.team_beef_stats_bool
+ or settings.report_settings.team_boom_or_bust_bool
+ )):
self.toc_team_section_data = [
[Paragraph("Teams", self.toc_style_title_right),
"",
diff --git a/requirements.txt b/requirements.txt
index bb21b8c2..4b08a71f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,19 +1,23 @@
beautifulsoup4==4.12.2
+camel-converter==3.1.0
colorama==0.4.6
-espn-api==0.32.0
+espn-api==0.33.0
GitPython==3.1.40
-google-api-python-client==2.104.0
+google-api-python-client==2.108.0
httplib2==0.22.0
-numpy==1.26.1
+numpy==1.26.2
oauth2client==4.1.3
Pillow==10.1.0
+pydantic==2.5.2
+pydantic-settings==2.1.0
PyDrive==1.3.1
-pytest==7.4.2
-reportlab==4.0.6
+pytest==7.4.3
+python-dotenv==1.0.0
+reportlab==4.0.7
requests==2.31.0
-selenium==4.14.0
+selenium==4.15.2
slack==0.0.2
slackclient==2.9.4
-urllib3==2.0.7
+urllib3==2.1.0
yahoo-oauth==2.0
-yfpy==13.0.0
+yfpy==15.0.3
diff --git a/resources/documentation/README.md b/resources/documentation/README.md
index ee683908..61fbca5b 100644
--- a/resources/documentation/README.md
+++ b/resources/documentation/README.md
@@ -105,7 +105,7 @@
*before running the report **EVERY TIME** you open a new command line prompt to run the application!*
-* Make sure you have updated the default league ID (`league_id` value) in the `config.ini` file to your own league id. Please see the respective setup instructions for your chosen platform for directions on how to find your league ID.
+* Make sure you have updated the default league ID (`league_id` value) in the `.env` file to your own league id. Please see the respective setup instructions for your chosen platform for directions on how to find your league ID.
* Run `python main.py`. You should see the following prompts:
@@ -129,7 +129,7 @@
* Assuming the above went as expected, the application should now generate a report for your fantasy league for the selected NFL week.
-***NOTE***: You can also specify a large number of configuration options directly in the command line. Please see the [usage section](../../README.md#usage) for more information.
+***NOTE***: You can also specify a large number of settings directly in the command line. Please see the [usage section](../../README.md#usage) for more information.
##### macOS Launch Script
diff --git a/resources/documentation/descriptions.py b/resources/documentation/descriptions.py
index a6c4ab3c..19691284 100644
--- a/resources/documentation/descriptions.py
+++ b/resources/documentation/descriptions.py
@@ -79,14 +79,6 @@
"here, and uses the total weight of each team's starting lineup, including the rolled-up " \
"weights of starting defenses, to give each team a total TABBU score."
-covid_19_risk = "The COVID-19 risk ranking is a \"just-for-fun\" metric that pulls NFL player transaction history " \
- "from USA Today " \
- "Sports NFL player transactions list, and then assigns a risk factor to every player based " \
- "whether or not that player is currently on the Reserve/COVID-19 list, whether or not that player has " \
- "previously been on the Reserve/COVID-19 list, how many teammates of that player have been on the " \
- "Reserve/COVID-19 list, and how recently the last teammate of that player was on the " \
- "Reserve/COVID-19 list."
-
weekly_top_scorers = "Running list of each week's highest scoring team. Can be used for weekly highest points payouts."
weekly_highest_coaching_efficiency = "Running list of each week's team with the highest coaching efficiency. Can be " \
diff --git a/resources/espn_quickstart.py b/resources/espn_quickstart.py
index ecceca14..7b565c9d 100644
--- a/resources/espn_quickstart.py
+++ b/resources/espn_quickstart.py
@@ -9,7 +9,7 @@
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
-from report.logger import get_logger
+from utilities.logger import get_logger
logger = get_logger(__file__)
@@ -79,7 +79,6 @@ def main():
except (TimeoutException, NoSuchElementException):
logger.info(f"Already logged in to Chrome with user profile \"{auth_json.get('chrome_user_profile')}\".")
-
# retrieve and display session cookies needed for ESPN FF API authentication and extract their values
swid_cookie, espn_s2_cookie = WebDriverWait(driver, timeout=60).until(lambda d: extract_espn_session_cookies(d))
logger.info(f"\"SWID\" session cookie: {swid_cookie}")
@@ -87,5 +86,6 @@ def main():
driver.quit()
+
if __name__ == "__main__":
main()
diff --git a/resources/files/crime_categories.json b/resources/files/crime_categories.json
index 527b1824..ab8ab2ef 100644
--- a/resources/files/crime_categories.json
+++ b/resources/files/crime_categories.json
@@ -79,7 +79,6 @@
"INVOLUNTARY MANSLAUGHTER": 4,
"LEAVING SCENE": 1,
"LICENSE": 1,
- "LICENSE, GUNS": 1,
"MANSLAUGHTER": 5,
"MANSLAUGHTER, CHILD ABUSE": 5,
"MENACING": 2,
@@ -120,5 +119,6 @@
"TRESPASSING": 1,
"VANDALISM": 1,
"VIOLATING COURT ORDER": 1,
+ "VIOLATING PROTECTIVE ORDER": 2,
"WEAPON": 1
}
\ No newline at end of file
diff --git a/resources/google_quickstart.py b/resources/google_quickstart.py
index 81764ea6..f7567979 100644
--- a/resources/google_quickstart.py
+++ b/resources/google_quickstart.py
@@ -6,7 +6,7 @@
from httplib2 import Http
from oauth2client import file, client, tools
-from report.logger import get_logger
+from utilities.logger import get_logger
logger = get_logger(__file__)
diff --git a/tests/test_features.py b/tests/test_features.py
index ffd7a8ad..01ee3489 100644
--- a/tests/test_features.py
+++ b/tests/test_features.py
@@ -5,18 +5,13 @@
import sys
from pathlib import Path
-from pytest import mark
-
module_dir = Path(__file__).parent.parent
sys.path.append(str(module_dir))
-from utilities.config import AppConfigParser
-
from calculate.bad_boy_stats import BadBoyStats
from calculate.beef_stats import BeefStats
-from calculate.covid_risk import CovidRisk
-from report.logger import get_logger
+from utilities.logger import get_logger
logger = get_logger(__file__)
@@ -24,9 +19,6 @@
if not Path(test_data_dir).exists():
os.makedirs(test_data_dir)
-config = AppConfigParser()
-config.read(Path(__file__).parent.parent / "config.ini")
-
player_first_name = "Marquise"
player_last_name = "Brown"
player_full_name = f"{player_first_name} {player_last_name}"
@@ -76,35 +68,11 @@ def test_beef_init():
assert beef_stats.beef_data is not None
-@mark.skip
-def test_covid_init():
- covid_risk = CovidRisk(
- config=config,
- data_dir=test_data_dir,
- season=2020,
- week=1,
- save_data=True,
- offline=False,
- refresh=True
- )
- covid_risk.generate_covid_risk_json()
-
- logger.info(
- f"\nCOVID-19 risk for {player_full_name}: "
- f"{covid_risk.get_player_covid_risk(player_full_name, player_team_abbr, player_position)}"
- )
-
- assert covid_risk.covid_data is not None
-
-
if __name__ == "__main__":
logger.info("Testing features...")
- # uncomment below function to test bad boy data retrieval
+ # test bad boy data retrieval
test_bad_boy_init()
- # uncomment below function to test player weight (beef) data retrieval
+ # test player weight (beef) data retrieval
test_beef_init()
-
- # uncomment below function to test player covid data retrieval
- test_covid_init()
diff --git a/utilities/app.py b/utilities/app.py
index 32d871fe..2d3257ab 100644
--- a/utilities/app.py
+++ b/utilities/app.py
@@ -2,17 +2,13 @@
__email__ = "uberfastman@uberfastman.dev"
import os
-import pkgutil
import re
-import shutil
import socket
import sys
import time
-from collections import defaultdict
from datetime import datetime
from pathlib import Path
from typing import List, Dict, Union, Any
-from calculate.metrics import CalculateMetrics
import colorama
import requests
@@ -23,16 +19,15 @@
from calculate.bad_boy_stats import BadBoyStats
from calculate.beef_stats import BeefStats
-from calculate.covid_risk import CovidRisk
-from dao import platforms
+from calculate.metrics import CalculateMetrics
from dao.base import BaseLeague, BaseTeam, BasePlayer
from dao.platforms.cbs import LeagueData as CbsLeagueData
from dao.platforms.espn import LeagueData as EspnLeagueData
from dao.platforms.fleaflicker import LeagueData as FleaflickerLeagueData
from dao.platforms.sleeper import LeagueData as SleeperLeagueData
from dao.platforms.yahoo import LeagueData as YahooLeagueData
-from report.logger import get_logger
-from utilities.config import AppConfigParser
+from utilities.logger import get_logger
+from utilities.settings import settings
from utilities.utils import format_platform_display
logger = get_logger(__name__, propagate=False)
@@ -46,12 +41,12 @@
current_month = current_date.month
-def user_week_input_validation(config: AppConfigParser, week: int, retrieved_current_week: int, season: int) -> int:
+def user_week_input_validation(week: int, retrieved_current_week: int, season: int) -> int:
# user input validation
if week:
week_for_report = week
else:
- week_for_report = config.get("Settings", "week_for_report")
+ week_for_report = settings.week_for_report
# only validate user week if report is being run for current season
if current_year == int(season) or (current_year == (int(season) + 1) and current_month < 9):
@@ -98,11 +93,11 @@ def user_week_input_validation(config: AppConfigParser, week: int, retrieved_cur
return int(week_for_report)
-def get_current_nfl_week(config: AppConfigParser, offline: bool) -> int:
+def get_current_nfl_week(offline: bool) -> int:
# api_url = "https://bet.rotoworld.com/api/nfl/calendar/game_count"
api_url = "https://api.sleeper.app/v1/state/nfl"
- current_nfl_week = int(config.getint("Settings", "current_week"))
+ current_nfl_week = settings.current_nfl_week
if not offline:
logger.debug("Retrieving current NFL week from the Sleeper API.")
@@ -111,23 +106,22 @@ def get_current_nfl_week(config: AppConfigParser, offline: bool) -> int:
nfl_weekly_info = requests.get(api_url).json()
current_nfl_week = nfl_weekly_info.get("week")
except (KeyError, ValueError) as e:
- logger.warning("Unable to retrieve current NFL week. Defaulting to value set in \"config.ini\".")
+ logger.warning("Unable to retrieve current NFL week. Defaulting to value set in \".env\" file.")
logger.debug(e)
else:
logger.debug("The Fantasy Football Metrics Weekly Report app is being run in offline mode. "
- "The current NFL week will default to the value set in \"config.ini\".")
+ "The current NFL week will default to the value set in \".env\" file.")
return current_nfl_week
-def league_data_factory(config: AppConfigParser, base_dir: Path, data_dir: Path, platform: str,
+def league_data_factory(base_dir: Path, data_dir: Path, platform: str,
game_id: Union[str, int], league_id: str, season: int, start_week: int, week_for_report: int,
save_data: bool, offline: bool) -> BaseLeague:
- if platform in get_supported_platforms():
+ if platform in settings.supported_platforms:
if platform == "yahoo":
yahoo_league = YahooLeagueData(
- config,
base_dir,
data_dir,
game_id,
@@ -144,7 +138,6 @@ def league_data_factory(config: AppConfigParser, base_dir: Path, data_dir: Path,
elif platform == "fleaflicker":
fleaflicker_league = FleaflickerLeagueData(
- config,
None,
data_dir,
league_id,
@@ -160,7 +153,6 @@ def league_data_factory(config: AppConfigParser, base_dir: Path, data_dir: Path,
elif platform == "sleeper":
sleeper_league = SleeperLeagueData(
- config,
None,
data_dir,
league_id,
@@ -176,7 +168,6 @@ def league_data_factory(config: AppConfigParser, base_dir: Path, data_dir: Path,
elif platform == "espn":
espn_league = EspnLeagueData(
- config,
base_dir,
data_dir,
league_id,
@@ -192,7 +183,6 @@ def league_data_factory(config: AppConfigParser, base_dir: Path, data_dir: Path,
elif platform == "cbs":
cbs_league = CbsLeagueData(
- config,
base_dir,
data_dir,
league_id,
@@ -209,23 +199,22 @@ def league_data_factory(config: AppConfigParser, base_dir: Path, data_dir: Path,
else:
logger.error(
f"Generating fantasy football reports for the \"{format_platform_display(platform)}\" fantasy football "
- f"platform is not currently supported. Please change your settings in config.ini and try again."
+ f"platform is not currently supported. Please change the settings in your .env file and try again."
)
sys.exit(1)
-def add_report_player_stats(config: AppConfigParser, season: int, metrics: Dict[str, Any], player: BasePlayer,
+def add_report_player_stats(metrics: Dict[str, Any], player: BasePlayer,
bench_positions: List[str]) -> BasePlayer:
player.bad_boy_crime = str()
player.bad_boy_points = int()
player.bad_boy_num_offenders = int()
player.weight = float()
player.tabbu = float()
- player.covid_risk = int()
if player.selected_position not in bench_positions:
- if config.getboolean("Report", "league_bad_boy_rankings"):
+ if settings.report_settings.league_bad_boy_rankings_bool:
bad_boy_stats: BadBoyStats = metrics.get("bad_boy_stats")
player.bad_boy_crime = bad_boy_stats.get_player_bad_boy_crime(
player.first_name, player.last_name, player.nfl_team_abbr, player.primary_position
@@ -237,27 +226,21 @@ def add_report_player_stats(config: AppConfigParser, season: int, metrics: Dict[
player.first_name, player.last_name, player.nfl_team_abbr, player.primary_position
)
- if config.getboolean("Report", "league_beef_rankings"):
+ if settings.report_settings.league_beef_rankings_bool:
beef_stats: BeefStats = metrics.get("beef_stats")
player.weight = beef_stats.get_player_weight(player.first_name, player.last_name, player.nfl_team_abbr)
player.tabbu = beef_stats.get_player_tabbu(player.first_name, player.last_name, player.nfl_team_abbr)
- if config.getboolean("Report", "league_covid_risk_rankings") and int(season) >= 2020:
- covid_risk: CovidRisk = metrics.get("covid_risk")
- player.covid_risk = covid_risk.get_player_covid_risk(
- player.full_name, player.nfl_team_abbr, player.primary_position)
-
return player
-def add_report_team_stats(config: AppConfigParser, team: BaseTeam, league: BaseLeague, week_counter: int, season: int,
- metrics_calculator: CalculateMetrics, metrics: Dict[str, Any], dq_ce: bool,
- inactive_players: List[str]) -> BaseTeam:
+def add_report_team_stats(team: BaseTeam, league: BaseLeague, week_counter: int, metrics_calculator: CalculateMetrics,
+ metrics: Dict[str, Any], dq_ce: bool, inactive_players: List[str]) -> BaseTeam:
team.name = metrics_calculator.decode_byte_string(team.name)
bench_positions = league.bench_positions
for player in team.roster:
- add_report_player_stats(config, season, metrics, player, bench_positions)
+ add_report_player_stats(metrics, player, bench_positions)
starting_lineup_points = round(
sum([p.points for p in team.roster if p.selected_position not in bench_positions]), 2)
@@ -270,7 +253,7 @@ def add_report_team_stats(config: AppConfigParser, team: BaseTeam, league: BaseL
team.bench_points = round(sum([p.points for p in team.roster if p.selected_position in bench_positions]), 2)
- if config.getboolean("Report", "league_bad_boy_rankings"):
+ if settings.report_settings.league_bad_boy_rankings_bool:
team.bad_boy_points = 0
team.worst_offense = None
team.num_offenders = 0
@@ -287,13 +270,10 @@ def add_report_team_stats(config: AppConfigParser, team: BaseTeam, league: BaseL
team.worst_offense = p.bad_boy_crime
team.worst_offense_score = p.bad_boy_points
- if config.getboolean("Report", "league_beef_rankings"):
+ if settings.report_settings.league_beef_rankings_bool:
team.total_weight = sum([p.weight for p in team.roster if p.selected_position not in bench_positions])
team.tabbu = sum([p.tabbu for p in team.roster if p.selected_position not in bench_positions])
- if config.getboolean("Report", "league_covid_risk_rankings") and int(season) >= 2020:
- team.total_covid_risk = sum([p.covid_risk for p in team.roster if p.selected_position not in bench_positions])
-
team.positions_filled_active = [p.selected_position for p in team.roster if
p.selected_position not in bench_positions]
@@ -317,13 +297,16 @@ def add_report_team_stats(config: AppConfigParser, team: BaseTeam, league: BaseL
def get_player_game_time_statuses(week: int, league: BaseLeague):
- file_name = "week_" + str(week) + "-player_status_data.html"
+ file_name = f"week_{week}-player_status_data.html"
file_dir = Path(league.data_dir) / str(league.season) / str(league.league_id) / f"week_{week}"
file_path = Path(file_dir) / file_name
if not league.offline:
- user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/605.1.15 (KHTML, like Gecko) " \
- "Version/13.0.2 Safari/605.1.15"
+ user_agent = (
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) "
+ "AppleWebKit/605.1.15 (KHTML, like Gecko) "
+ "Version/13.0.2 Safari/605.1.15"
+ )
headers = {
"user-agent": user_agent
}
@@ -333,7 +316,9 @@ def get_player_game_time_statuses(week: int, league: BaseLeague):
"type": "reg"
}
- response = requests.get("https://www.footballdb.com/transactions/injuries.html", headers=headers, params=params)
+ response = requests.get(
+ "https://www.footballdb.com/transactions/injuries.html", headers=headers, params=params
+ )
html_soup = BeautifulSoup(response.text, "html.parser")
logger.debug(f"Response URL: {response.url}")
@@ -358,13 +343,6 @@ def get_player_game_time_statuses(week: int, league: BaseLeague):
return html_soup
-def get_supported_platforms() -> List[str]:
- supported_platforms = []
- for module in pkgutil.iter_modules(platforms.__path__):
- supported_platforms.append(module.name)
- return supported_platforms
-
-
# function taken from https://stackoverflow.com/a/33117579 (written by 7h3rAm)
def active_network_connection(host: str = "8.8.8.8", port: int = 53, timeout: int = 3):
"""
@@ -396,180 +374,6 @@ def __init__(self, *args, **kwargs):
poolmanager.pool_classes_by_scheme['https'] = MyHTTPSConnectionPool
-def get_valid_config(config_file: str = "config.ini") -> AppConfigParser:
- config = AppConfigParser()
-
- root_directory = Path(__file__).parent.parent
-
- config_file_path = root_directory / Path(config_file)
-
- # set local config file (check for existence and access, stop app if it does not exist or cannot access)
- if config_file_path.is_file():
- if os.access(config_file_path, mode=os.R_OK):
- logger.debug(
- "Configuration file \"config.ini\" available. Running Fantasy Football Metrics Weekly Report app...")
- config_template = AppConfigParser()
- config_template.read(root_directory / "config.template.ini")
- config.read(config_file_path)
-
- if not set(config_template.sections()).issubset(set(config.sections())):
- missing_sections = []
- for section in config_template.sections():
- if section not in config.sections():
- missing_sections.append(section)
-
- logger.error(
- f"Your local \"config.ini\" file is missing the following sections:\n\n"
- f"{', '.join(missing_sections)}\n\nPlease update your \"config.ini\" with the above sections from "
- f"\"config.template.ini\" and try again."
- )
- sys.exit(1)
- else:
- logger.debug("All required local \"config.ini\" sections present.")
-
- config_map = defaultdict(set)
- for section in config.sections():
- section_keys = set()
- for (k, v) in config.items(section):
- section_keys.add(k)
- config_map[section] = section_keys
-
- missing_keys_map = defaultdict(set)
- for section in config_template.sections():
- if section in config.sections():
- for (k, v) in config_template.items(section):
- if k not in config_map.get(section):
- missing_keys_map[section].add(k)
-
- if missing_keys_map:
- missing_keys_str = ""
- for section, keys in missing_keys_map.items():
- missing_keys_str += f"Section: {section}\n"
- for key in keys:
- missing_keys_str += f" {key}\n"
- missing_keys_str += "\n"
- missing_keys_str = missing_keys_str.strip()
-
- logger.error(
- f"Your local \"config.ini\" file is missing the following keys (from their respective sections):"
- f"\n\n{missing_keys_str}\n\nPlease update your \"config.ini\" with the above keys from "
- f"\"config.template.ini\" and try again."
- )
-
- sys.exit(1)
- else:
- logger.debug("All required local \"config.ini\" keys present.")
-
- return config
- else:
- logger.error(
- "Unable to access configuration file \"config.ini\". "
- "Please check that file permissions are properly set.")
- sys.exit(1)
- else:
- logger.debug("Configuration file \"config.ini\" not found.")
- create_config = input(
- f"{Fore.RED}Configuration file \"config.ini\" not found. {Fore.GREEN}Do you wish to create one? "
- f"{Fore.YELLOW}({Fore.GREEN}y{Fore.YELLOW}/{Fore.RED}n{Fore.YELLOW}) -> {Style.RESET_ALL}"
- )
- if create_config == "y":
- return create_config_from_template(config, root_directory, config_file_path)
- if create_config == "n":
- logger.error(
- "Configuration file \"config.ini\" not found. "
- "Please make sure that it exists in project root directory.")
- sys.exit(1)
- else:
- logger.warning("Please only select \"y\" or \"n\".")
- time.sleep(0.25)
- get_valid_config(config_file)
-
-
-def create_config_from_template(config: AppConfigParser, root_directory: Path, config_file_path: Path,
- platform: str = None, league_id: str = None, season: int = None,
- current_week: int = None) -> AppConfigParser:
- logger.debug("Creating \"config.ini\" file from template.")
- config_template_file = Path(root_directory) / "config.template.ini"
- config_file_path = shutil.copyfile(config_template_file, config_file_path)
-
- config.read(config_file_path)
-
- if not platform:
- supported_platforms = get_supported_platforms()
- platform = input(
- f"{Fore.GREEN}For which fantasy football platform are you generating a report? "
- f"({'/'.join(supported_platforms)}) -> {Style.RESET_ALL}"
- )
- if platform not in supported_platforms:
- logger.warning(
- f"Please only select one of the following platforms: "
- f"{', or '.join([', '.join(supported_platforms[:-1]), supported_platforms[-1]])}"
- )
- time.sleep(0.25)
- config = create_config_from_template(config, root_directory, config_file_path)
- logger.debug(f"Retrieved fantasy football platform for \"config.ini\": {platform}")
-
- config.set("Settings", "platform", platform)
-
- if not league_id:
- league_id = input(f"{Fore.GREEN}What is your league ID? -> {Style.RESET_ALL}")
- logger.debug(f"Retrieved fantasy football league ID for \"config.ini\": {league_id}")
-
- config.set("Settings", "league_id", league_id)
-
- if not season:
- season = input(
- f"{Fore.GREEN}For which NFL season (starting year of season) are you generating reports? -> "
- f"{Style.RESET_ALL}"
- )
- try:
- if int(season) > current_year:
- logger.warning("This report cannot predict the future. Please only input a current or past NFL season.")
- time.sleep(0.25)
- config = create_config_from_template(config, root_directory, config_file_path, platform=platform,
- league_id=league_id)
- elif int(season) < 2019 and platform == "espn":
- logger.warning("ESPN leagues prior to 2019 are not supported. Please select a later NFL season.")
- time.sleep(0.25)
- config = create_config_from_template(config, root_directory, config_file_path, platform=platform,
- league_id=league_id)
-
- except ValueError:
- logger.warning("You must input a valid year in the format YYYY.")
- time.sleep(0.25)
- config = create_config_from_template(config, root_directory, config_file_path, platform=platform,
- league_id=league_id)
- logger.debug(f"Retrieved fantasy football season for \"config.ini\": {season}")
-
- config.set("Settings", "season", season)
-
- if not current_week:
- current_week = input(
- f"{Fore.GREEN}What is the current week of the NFL season? (week following the last complete week) -> "
- f"{Style.RESET_ALL}"
- )
- try:
- if int(current_week) < 0 or int(current_week) > NFL_SEASON_LENGTH:
- logger.warning(
- f"Week {current_week} is not a valid NFL week. Please select a week from 1 to {NFL_SEASON_LENGTH}.")
- time.sleep(0.25)
- config = create_config_from_template(config, root_directory, config_file_path, platform=platform,
- league_id=league_id, season=season)
- except ValueError:
- logger.warning("You must input a valid integer to represent the current NFL week.")
- time.sleep(0.25)
- config = create_config_from_template(config, root_directory, config_file_path, platform=platform,
- league_id=league_id, season=season)
- logger.debug(f"Retrieved current NFL week for \"config.ini\": {current_week}")
-
- config.set("Settings", "current_week", current_week)
-
- with open(config_file_path, "w") as cf:
- config.write(cf, space_around_delimiters=True)
-
- return config
-
-
# function taken from https://stackoverflow.com/a/35585837 (written by morxa)
def git_ls_remote(url: str):
remote_refs = {}
@@ -584,12 +388,17 @@ def check_for_updates(use_default: bool = False):
logger.debug("Checking upstream remote for app updates.")
project_repo = Repo(Path(__file__).parent.parent)
+ project_repo.remote().update()
+
if not active_network_connection():
logger.info(
"No active network connection found. Unable to check for updates for the Fantasy Football Metrics Weekly "
"Report app."
)
else:
+ project_repo.remote(name="origin").update()
+ project_repo.remote(name="origin").fetch(prune=True)
+
version_tags = sorted(
[tag_ref for tag_ref in project_repo.tags if hasattr(tag_ref.tag, "tagged_date")],
key=lambda x: x.tag.tagged_date,
@@ -722,7 +531,5 @@ def update_app(repository: Repo):
if __name__ == "__main__":
- local_config = AppConfigParser()
- local_config.read("../config.ini")
- local_current_nfl_week = get_current_nfl_week(config=local_config, offline=False)
+ local_current_nfl_week = get_current_nfl_week(offline=False)
logger.info(f"Local current NFL week: {local_current_nfl_week}")
diff --git a/utilities/config.py b/utilities/config.py
deleted file mode 100644
index e4491a9e..00000000
--- a/utilities/config.py
+++ /dev/null
@@ -1,136 +0,0 @@
-import logging
-import os
-import re
-from collections import defaultdict
-from configparser import ConfigParser, NoOptionError, NoSectionError
-from pathlib import Path
-from typing import Dict, Iterable, Union
-
-logger = logging.getLogger(__name__)
-
-# Used in parser getters to indicate the default behaviour when a specific
-# option is not found it to raise an exception. Created to enable `None` as
-# a valid fallback value.
-_UNSET = object()
-
-_default_dict = dict
-DEFAULTSECT = "DEFAULT"
-
-
-class AppConfigParser(ConfigParser):
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- self.comment_map = defaultdict(dict)
-
- # noinspection PyShadowingBuiltins
- def get(self, section: str, option: str, *, raw=False, vars=None, fallback=_UNSET):
- """Get an option value for a given section.
-
- If `vars' is provided, it must be a dictionary. The option is looked up
- in `vars' (if provided), `section', and in `DEFAULTSECT' in that order.
- If the key is not found and `fallback' is provided, it is used as
- a fallback value. `None' can be provided as a `fallback' value.
-
- If interpolation is enabled and the optional argument `raw' is False,
- all interpolations are expanded in the return values.
-
- Arguments `raw', `vars', and `fallback` are keyword only.
-
- The section DEFAULT is special.
- """
- try:
- d = self._unify_values(section, vars)
- except NoSectionError:
- if fallback is _UNSET:
- raise
- else:
- return fallback
- option = self.optionxform(option)
- try:
- value = d[option]
- except KeyError:
-
- if fallback is _UNSET:
- if (section == "Report"
- and (str(option).startswith("league")
- or str(option).startswith("report")
- or str(option).startswith("team"))):
- logger.warning(
- f"MISSING CONFIGURATION VALUE: \"{section}: {option}\"! Setting to default value of \"False\". "
- f"To include this section, update \"config.ini\" and try again."
- )
- return "False"
- else:
- raise NoOptionError(option, section)
- else:
- return fallback
-
- if raw or value is None:
- return value
- else:
- return self._interpolation.before_get(self, section, option, value, d)
-
- def read(self, filenames: Union[str, Path, Iterable[str]], encoding: str = None):
- """Read and parse a filename or an iterable of filenames.
-
- Files that cannot be opened are silently ignored; this is
- designed so that you can specify an iterable of potential
- configuration file locations (e.g. current directory, user's
- home directory, system-wide directory), and all existing
- configuration files in the iterable will be read. A single
- filename may also be given.
-
- Return list of successfully read files.
- """
- if isinstance(filenames, (str, bytes, os.PathLike)):
- filenames = [filenames]
- read_ok = []
- for filename in filenames:
- try:
- with open(filename, encoding=encoding) as fp:
- section = None
- key_comments = []
- lines = fp.readlines()
- for line in lines:
- if str(line).startswith("["):
- section = re.sub("[\\W_]+", "", line)
- self.comment_map[section] = {}
- else:
- if str(line).startswith(";"):
- key_comments.append(line)
- else:
- if "=" in line:
- key = line.split("=")[0].strip()
- self.comment_map[section][key] = key_comments
- key_comments = []
- fp.seek(0)
- self._read(fp, filename)
- except OSError:
- continue
- if isinstance(filename, os.PathLike):
- filename = os.fspath(filename)
- read_ok.append(filename)
- return read_ok
-
- def _write_section(self, fp, section_name: str, section_items: Dict[str, str], delimiter: str):
- """Write a single section to the specified `fp`."""
- fp.write(f"[{section_name}]\n")
- section_comments_map = self.comment_map.get(section_name)
-
- for key, value in section_items:
- value = self._interpolation.before_write(self, section_name, key, value)
- if value is not None or not self._allow_no_value:
- value = delimiter + str(value).replace('\n', '\n\t')
- else:
- value = ""
-
- if key in section_comments_map.keys():
-
- key_comments = section_comments_map.get(key)
- for comment in key_comments:
- fp.write(comment)
-
- fp.write(f"{key}{value}\n")
- fp.write("\n")
diff --git a/utilities/constants.py b/utilities/constants.py
new file mode 100644
index 00000000..0ebe38b3
--- /dev/null
+++ b/utilities/constants.py
@@ -0,0 +1,42 @@
+
+from typing import List, Dict
+
+# nfl team abbreviations
+nfl_team_abbreviations: List[str] = [
+ "ARI", "ATL", "BAL", "BUF", "CAR", "CHI", "CIN", "CLE",
+ "DAL", "DEN", "DET", "GB", "HOU", "IND", "JAX", "KC",
+ "LAR", "LAC", "LV", "MIA", "MIN", "NE", "NO", "NYG",
+ "NYJ", "PHI", "PIT", "SEA", "SF", "TB", "TEN", "WAS"
+]
+
+# small reference dict to convert between commonly used alternate team abbreviations
+nfl_team_abbreviation_conversions: Dict[str, str] = {
+ "JAC": "JAX",
+ "LA": "LAR",
+ "WSH": "WAS"
+}
+
+# prohibited player statuses to check team coaching efficiency eligibility if dq_ce = True
+prohibited_statuses = {
+ "O": "Out",
+ "Out": "Out",
+ "NA": "Inactive: Coach's Decision or Not on Roster",
+ "INACTIVE": "Inactive: Coach's Decision or Not on Roster",
+ "IR-R": "Injured Reserve - Designated for Return",
+ "IR": "Injured Reserve",
+ "COVID-19": "Reserve: COVID-19",
+ "SUSP": "Suspended",
+ "Reserve-Sus": "Suspended",
+ "DNR": "Reserve: Did Not Report",
+ "PUP-P": "Physically Unable to Perform (Preseason)",
+ "PUP-R": "Physically Unable to Perform (Regular Season)",
+ "NFI": "Non-Football Injury",
+ "NFI-A": "Non-Football Injury (Active)",
+ "NFI-R": "Non-Football Injury (Reserve)",
+ "EX": "Reserve: Exemption",
+ "Reserve-Ex": "Reserve: Exemption",
+ "CEL": "Reserve: Commissioner Exempt List",
+ "Reserve-CEL": "Reserve: Commissioner Exempt List",
+ "RET": "Reserve: Retired",
+ "Reserve-Ret": "Reserve: Retired"
+}
diff --git a/report/logger.py b/utilities/logger.py
similarity index 92%
rename from report/logger.py
rename to utilities/logger.py
index 0fd4d39d..d4c884ab 100644
--- a/report/logger.py
+++ b/utilities/logger.py
@@ -4,20 +4,19 @@
import re
import sys
import time
-from configparser import NoSectionError
# from logging.handlers import RotatingFileHandler
from logging.handlers import TimedRotatingFileHandler
from pathlib import Path
import colorama
from colorama import Fore, Style
+from dotenv import load_dotenv
-from utilities.config import AppConfigParser
+load_dotenv(Path(__file__).parent.parent / ".env")
colorama.init()
-config = AppConfigParser()
-config.read(Path(__file__).parent.parent / "config.ini")
+PROJECT_ROOT = Path(__file__).parent.parent
class StyledFormatter(logging.Formatter):
@@ -138,14 +137,10 @@ def get_logger(module_name=None, propagate=True):
"critical": logging.CRITICAL
}
- try:
- log_level = log_level_mapping[config.get("Configuration", "log_level")]
- except (KeyError, NoSectionError):
- log_level = logging.INFO
+ log_level = log_level_mapping.get(os.environ.get("LOG_LEVEL", "info") or "info")
- log_file_dir = "logs"
- log_file_name = "out.log"
- log_file = Path(log_file_dir) / log_file_name
+ log_file_dir = PROJECT_ROOT / "logs"
+ log_file = log_file_dir / "out.log"
if not Path(log_file_dir).exists():
os.makedirs(log_file_dir)
@@ -207,7 +202,7 @@ def get_logger(module_name=None, propagate=True):
handler = SizedTimedRotatingFileHandler(
log_filename,
when="s", # s = seconds, m = minutes, h = hours, midnight = at midnight, etc.
- interval=3, # how many increments of the "when" configuration to wait before creating next log file
+ interval=3, # how many increments of the "when" parameter value to wait before creating next log file
maxBytes=100,
backupCount=5,
# encoding='bz2', # uncomment for bz2 compression
diff --git a/utilities/settings.py b/utilities/settings.py
new file mode 100644
index 00000000..0a44b49c
--- /dev/null
+++ b/utilities/settings.py
@@ -0,0 +1,475 @@
+import json
+import os
+import sys
+from datetime import datetime
+from inspect import isclass
+from pathlib import Path
+from time import sleep
+from typing import List, Dict, Set, Tuple, Type, Any, Union, Optional
+
+from camel_converter import to_snake
+from colorama import Fore, Style
+from dotenv import dotenv_values
+from pydantic import Field
+from pydantic.fields import FieldInfo
+from pydantic_settings import BaseSettings, SettingsConfigDict, EnvSettingsSource, PydanticBaseSettingsSource
+
+from utilities.logger import get_logger
+
+root_directory = Path(__file__).parent.parent
+
+logger = get_logger(__name__)
+
+NFL_SEASON_LENGTH = 18
+
+current_date = datetime.today()
+current_year = current_date.year
+current_month = current_date.month
+
+
+class CustomSettingsSource(EnvSettingsSource):
+
+ def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool) -> Any:
+ if value is None:
+ return None
+ if field_name == "league_id":
+ return str(value)
+ if field_name == "week_for_report":
+ try:
+ return int(value)
+ except (ValueError, TypeError):
+ return value
+ if field_name in ["supported_platforms", "supported_fonts", "coaching_efficiency_disqualified_teams"]:
+ return value.split(",") if value else []
+ if field_name.endswith("_bool"):
+ return str(value).lower() == "true"
+ if field_name.endswith("_local_path"):
+ return Path(value)
+ if isinstance(value, str):
+ return value or None
+ else:
+ return json.loads(value)
+
+
+class CustomSettings(BaseSettings):
+
+ @classmethod
+ def settings_customise_sources(
+ cls,
+ settings_cls: Type[BaseSettings],
+ init_settings: PydanticBaseSettingsSource,
+ env_settings: PydanticBaseSettingsSource,
+ dotenv_settings: PydanticBaseSettingsSource,
+ file_secret_settings: PydanticBaseSettingsSource,
+ ) -> Tuple[PydanticBaseSettingsSource, ...]:
+ return (CustomSettingsSource(settings_cls),)
+
+ def __repr__(self):
+ properties = ", ".join([f"{k}={v}" for k, v in self.__dict__.items()])
+ return f"{self.__class__.__name__}({properties})"
+
+ def __str__(self):
+ properties = ", ".join([f"{k}={v}" for k, v in self.__dict__.items()])
+ return f"{self.__class__.__name__}({properties})"
+
+ @classmethod
+ def get_fields(cls, parent_cls=None) -> Set[Tuple[str, str, str]]:
+ settings_field_keys = set()
+ for k, v in cls.model_fields.items():
+ try:
+ if issubclass(v.annotation, CustomSettings):
+ settings_field_keys.update(
+ [(str(field[0]).upper(), field[1], field[2]) for field in v.annotation.get_fields(cls.__name__)]
+ )
+ else:
+ settings_field_keys.add((str(k).upper(), v.title, parent_cls))
+ except TypeError:
+ settings_field_keys.add((str(k).upper(), v.title, parent_cls))
+
+ return settings_field_keys
+
+ def convert_to_default_values(self):
+ for field_key, field in self.model_fields.items():
+ setattr(self, field_key, field.default)
+
+ def get_fields_by_title_group(self) -> Dict[str, Dict[str, Field]]:
+
+ fields_by_title = {}
+ field_values = self.model_dump()
+ for field_key, field in self.model_fields.items():
+ if isclass(field.annotation) and issubclass(field.annotation, CustomSettings):
+ fields_by_title.update(**field.default.get_fields_by_title_group())
+ else:
+ field_title_key = to_snake(field.title)
+ if field_title_key in fields_by_title.keys():
+ fields_by_title[field_title_key][field_key] = (field_values[field_key], field.description)
+ else:
+ fields_by_title[field_title_key] = {field_key: (field_values[field_key], field.description)}
+
+ return fields_by_title
+
+
+class PlatformSettings(CustomSettings):
+ # yahoo
+ yahoo_game_id: Union[str, int] = Field(
+ "nfl",
+ title=__qualname__,
+ description=(
+ "YAHOO LEAGUES ONLY: game_id can be either `nfl`, in which case Yahoo defaults to using the current "
+ "season, or it can be a specific Yahoo game id for a specific season, such as 331 (2014 NFL season), 380 "
+ "(2018 NFL season), or 390 (2019 nfl season)"
+ )
+ )
+ yahoo_auth_dir_local_path: Path = Field(Path("auth/yahoo"), title=__qualname__)
+ yahoo_initial_faab_budget: int = Field(
+ 100,
+ title=__qualname__,
+ description="YAHOO LEAGUES ONLY: default FAAB since the initial/starting FAAB is not exposed in the API"
+ )
+
+ # espn
+ espn_auth_dir_local_path: Path = Field(Path("auth/espn"), title=__qualname__)
+
+ # cbs
+ cbs_auth_dir_local_path: Path = Field(Path("auth/cbs"), title=__qualname__)
+
+
+class ReportSettings(CustomSettings):
+ league_standings_bool: bool = Field(True, title=__qualname__)
+ league_playoff_probs_bool: bool = Field(True, title=__qualname__)
+ league_median_standings_bool: bool = Field(True, title=__qualname__)
+ league_power_rankings_bool: bool = Field(True, title=__qualname__)
+ league_z_score_rankings_bool: bool = Field(True, title=__qualname__)
+ league_score_rankings_bool: bool = Field(True, title=__qualname__)
+ league_coaching_efficiency_rankings_bool: bool = Field(True, title=__qualname__)
+ league_luck_rankings_bool: bool = Field(True, title=__qualname__)
+ league_optimal_score_rankings_bool: bool = Field(True, title=__qualname__)
+ league_bad_boy_rankings_bool: bool = Field(True, title=__qualname__)
+ league_beef_rankings_bool: bool = Field(True, title=__qualname__)
+ league_weekly_top_scorers_bool: bool = Field(True, title=__qualname__)
+ league_weekly_highest_ce_bool: bool = Field(True, title=__qualname__)
+ report_time_series_charts_bool: bool = Field(True, title=__qualname__)
+ report_team_stats_bool: bool = Field(True, title=__qualname__)
+ team_points_by_position_charts_bool: bool = Field(True, title=__qualname__)
+ team_bad_boy_stats_bool: bool = Field(True, title=__qualname__)
+ team_beef_stats_bool: bool = Field(True, title=__qualname__)
+ team_boom_or_bust_bool: bool = Field(True, title=__qualname__)
+
+ font: str = Field(
+ "helvetica",
+ title=__qualname__,
+ description="set font for report (defaults to Helvetica)"
+ )
+ supported_fonts: List[str] = Field(
+ ["helvetica", "times", "symbola", "opensansemoji", "sketchcollege", "leaguegothic"],
+ title=__qualname__,
+ description="supported fonts (comma-delimited list with no spaces)"
+ )
+ font_size: int = Field(
+ 12,
+ title=__qualname__,
+ description="set base font size (certain report element fonts resize dynamically based on the base font size)"
+ )
+ image_quality: int = Field(
+ 75,
+ title=__qualname__,
+ description=(
+ "specify player headshot image quality in percent (default: 75%), where higher quality (up to 100%) "
+ "results in a larger file size for the PDF report"
+ )
+ )
+ max_data_chars: int = Field(
+ 24,
+ title=__qualname__,
+ description="specify max number of characters to display for any given data cell in the report tables"
+ )
+
+
+class IntegrationSettings(CustomSettings):
+ # google drive
+ google_drive_upload_bool: bool = Field(
+ False,
+ title=__qualname__,
+ description=(
+ "change GOOGLE_DRIVE_UPLOAD_BOOL to True/False to turn on/off uploading of the report to Google Drive"
+ )
+ )
+ google_drive_auth_token_local_path: Path = Field(Path("auth/google/token.json"), title=__qualname__)
+ google_drive_reupload_file_local_path: Optional[Path] = Field(
+ "resources/files/example_report.pdf",
+ title=__qualname__
+ )
+ google_drive_default_folder_path: str = Field("Fantasy_Football", title=__qualname__)
+ google_drive_folder_path: Optional[str] = Field(None, title=__qualname__)
+
+ # slack
+ slack_post_bool: bool = Field(
+ False,
+ title=__qualname__,
+ description="change SLACK_POST_BOOL to True/False to turn on/off posting of the report to Slack"
+ )
+ slack_auth_token_local_path: Path = Field(Path("auth/slack/token.json"), title=__qualname__)
+ slack_repost_file_local_path: Optional[Path] = Field(
+ "resources/files/example_report.pdf",
+ title=__qualname__
+ )
+ slack_post_or_file: str = Field(
+ "file",
+ title=__qualname__,
+ description=(
+ "options for SLACK_POST_OR_FILE: post (if you wish to post a link to the report), file (if you wish to "
+ "post the report PDF)"
+ )
+ )
+ slack_channel: Optional[str] = Field(None, title=__qualname__)
+ slack_channel_notify_bool: bool = Field(False, title=__qualname__)
+
+
+class AppSettings(CustomSettings):
+ model_config = SettingsConfigDict(
+ # env_file=".env",
+ # env_file_encoding="utf-8",
+ validate_default=False,
+ extra="ignore" # allow, forbid, or ignore
+ )
+
+ log_level: str = Field(
+ "info",
+ title=__qualname__,
+ description="logger output level: notset, debug, info, warning, error, critical"
+ )
+ # output directories can be set to store your saved data and generated reports wherever you want
+ data_dir_local_path: Path = Field(
+ Path("output/data"),
+ title=__qualname__,
+ description="output directory can be set to store your saved data wherever you want"
+ )
+ output_dir_local_path: Path = Field(
+ Path("output/reports"),
+ title=__qualname__,
+ description="output directory can be set to store your generated reports wherever you want"
+ )
+
+ platform: Optional[str] = Field(
+ None,
+ env="PLATFORM",
+ validate_default=False,
+ title=__qualname__,
+ description="fantasy football platform for which you are running the report"
+ )
+ supported_platforms: List[str] = Field(
+ ["yahoo", "espn", "sleeper", "fleaflicker", "cbs"],
+ validate_default=False,
+ title=__qualname__,
+ description="supported fantasy football platforms"
+ )
+ league_id: Optional[str] = Field(
+ None,
+ title=__qualname__,
+ description=(
+ "example Yahoo public league archive for reference: https://archive.fantasysports.yahoo.com/nfl/2014/729259"
+ )
+ )
+ season: Optional[int] = Field(None, title=__qualname__)
+ current_nfl_week: Optional[int] = Field(None, title=__qualname__)
+ week_for_report: Union[int, str, None] = Field(
+ "default",
+ title=__qualname__,
+ description="value can be \"default\" or an integer between 1 and 18 defining the chosen week"
+ )
+ num_playoff_simulations: int = Field(
+ 10000,
+ title=__qualname__,
+ description=(
+ "select how many Monte Carlo simulations are used for playoff predictions, keeping in mind that while more "
+ "simulations improves the quality of the playoff predictions, it also makes this step of the report "
+ "generation take longer to complete"
+ )
+ )
+ num_playoff_slots: int = Field(
+ 6,
+ title=__qualname__,
+ description="FLEAFLICKER: default if number of playoff slots cannot be scraped"
+ )
+ num_playoff_slots_per_division: int = Field(
+ 1,
+ title=__qualname__,
+ description="number of top ranked teams that make the playoffs from each division (for leagues with divisions)"
+ )
+ num_regular_season_weeks: int = Field(
+ 14,
+ title=__qualname__,
+ description="SLEEPER/FLEAFLICKER: default if number of regular season weeks cannot be scraped/retrieved"
+ )
+ coaching_efficiency_disqualified_teams: List[str] = Field(
+ [],
+ title=__qualname__,
+ description=(
+ "multiple teams can be manually disqualified from coaching efficiency eligibility (comma-delimited list "
+ "with no spaces), for example: COACHING_EFFICIENCY_DISQUALIFIED_TEAMS=Team One,Team Two"
+ )
+ )
+
+ platform_settings: PlatformSettings = PlatformSettings()
+ report_settings: ReportSettings = ReportSettings()
+ integration_settings: IntegrationSettings = IntegrationSettings()
+
+
+def get_app_settings_from_env_file(env_file: str = None) -> AppSettings:
+ env_file_path = Path(env_file) if env_file else root_directory / Path(".env")
+ env_fields = AppSettings.get_fields()
+
+ # set local .env file (check for existence and access, stop app if it does not exist or cannot access)
+ if env_file_path.is_file():
+ if os.access(env_file_path, mode=os.R_OK):
+
+ env_vars_from_file = set(dotenv_values(env_file_path).keys())
+ missing_env_vars = set([field[0] for field in env_fields]).difference(env_vars_from_file)
+
+ if missing_env_vars:
+ logger.error(
+ f"Your local \".env\" file is missing the following variables:\n\n"
+ f"{', '.join(missing_env_vars)}\n\n"
+ f"Please update your \".env\" file and try again."
+ )
+ sys.exit(1)
+ else:
+ logger.debug("All required local \".env\" file variables present.")
+
+ logger.debug("The \".env\" file is available. Running Fantasy Football Metrics Weekly Report app...")
+
+ return AppSettings(
+ _env_file=env_file_path,
+ _env_file_encoding="utf-8"
+ )
+ else:
+ logger.error("Unable to access \".env\" file. Please check that file permissions are properly set.")
+ sys.exit(1)
+
+ else:
+ logger.debug("Local \".env\" file not found.")
+ create_env_file = input(
+ f"{Fore.RED}Local \".env\" file not found. {Fore.GREEN}Do you wish to create one? "
+ f"{Fore.YELLOW}({Fore.GREEN}y{Fore.YELLOW}/{Fore.RED}n{Fore.YELLOW}) -> {Style.RESET_ALL}"
+ ).lower()
+ if create_env_file == "y":
+ return create_env_file_from_settings(env_fields, env_file_path)
+ elif create_env_file == "n":
+ logger.error("Local \".env\" not found. Please make sure that it exists in project root directory.")
+ sys.exit(1)
+ else:
+ logger.warning("Please only select \"y\" or \"n\".")
+ sleep(0.25)
+ return get_app_settings_from_env_file(env_file)
+
+
+def create_env_file_from_settings(env_fields: Set[Tuple[str, str, str]], env_file_path: Path, platform: str = None,
+ league_id: str = None, season: int = None, current_week: int = None) -> AppSettings:
+ logger.debug("Creating \".env\" file from settings.")
+
+ app_settings = AppSettings(
+ _env_file=env_file_path,
+ _env_file_encoding="utf-8"
+ )
+ app_settings.convert_to_default_values()
+
+ if not platform:
+ supported_platforms = app_settings.supported_platforms
+ platform = input(
+ f"{Fore.GREEN}For which fantasy football platform are you generating a report? "
+ f"({'/'.join(supported_platforms)}) -> {Style.RESET_ALL}"
+ ).lower()
+ if platform not in supported_platforms:
+ logger.warning(
+ f"Please only select one of the following platforms: "
+ f"{', or '.join([', '.join(supported_platforms[:-1]), supported_platforms[-1]])}"
+ )
+ sleep(0.25)
+ create_env_file_from_settings(env_fields, env_file_path)
+
+ logger.debug(f"Retrieved fantasy football platform for \".env\" file: {platform}")
+
+ app_settings.platform = platform
+
+ if not league_id:
+ league_id = input(f"{Fore.GREEN}What is your league ID? -> {Style.RESET_ALL}")
+ logger.debug(f"Retrieved fantasy football league ID for \".env\" file: {league_id}")
+
+ app_settings.league_id = league_id
+
+ if not season:
+ season = input(
+ f"{Fore.GREEN}For which NFL season (starting year of season) are you generating reports? -> "
+ f"{Style.RESET_ALL}"
+ )
+ try:
+ if int(season) > current_year:
+ logger.warning("This report cannot predict the future. Please only input a current or past NFL season.")
+ sleep(0.25)
+ return create_env_file_from_settings(
+ env_fields, env_file_path, platform=platform, league_id=league_id
+ )
+ elif int(season) < 2019 and platform == "espn":
+ logger.warning("ESPN leagues prior to 2019 are not supported. Please select a later NFL season.")
+ sleep(0.25)
+ return create_env_file_from_settings(
+ env_fields, env_file_path, platform=platform, league_id=league_id
+ )
+
+ except ValueError:
+ logger.warning("You must input a valid year in the format YYYY.")
+ sleep(0.25)
+ return create_env_file_from_settings(env_fields, env_file_path, platform=platform, league_id=league_id)
+
+ logger.debug(f"Retrieved fantasy football season for \".env\" file: {season}")
+
+ app_settings.season = int(season)
+
+ if not current_week:
+ current_week = input(
+ f"{Fore.GREEN}What is the current week of the NFL season? (week following the last complete week) -> "
+ f"{Style.RESET_ALL}"
+ )
+ try:
+ if int(current_week) < 0 or int(current_week) > NFL_SEASON_LENGTH:
+ logger.warning(
+ f"Week {current_week} is not a valid NFL week. Please select a week from 1 to {NFL_SEASON_LENGTH}.")
+ sleep(0.25)
+ return create_env_file_from_settings(
+ env_fields, env_file_path, platform=platform, league_id=league_id, season=season
+ )
+ except ValueError:
+ logger.warning("You must input a valid integer to represent the current NFL week.")
+ sleep(0.25)
+ return create_env_file_from_settings(
+ env_fields, env_file_path, platform=platform, league_id=league_id, season=season
+ )
+
+ logger.debug(f"Retrieved current NFL week for \".env\" file: {current_week}")
+
+ app_settings.current_nfl_week = int(current_week)
+
+ with open(env_file_path, "w") as ef:
+
+ for field_type, fields in app_settings.get_fields_by_title_group().items():
+ ef.write(f"\n# # # # # {field_type.replace('_', ' ').upper()} # # # # #\n\n")
+
+ for field_key, (field_value, field_description) in fields.items():
+
+ if isinstance(field_value, list):
+ field_env_var_value = ",".join([val for val in field_value])
+ elif isinstance(field_value, bool) or field_value:
+ field_env_var_value = field_value
+ else:
+ field_env_var_value = ""
+ if field_description:
+ ef.write(f"# {field_description}\n")
+ ef.write(f"{str(field_key).upper()}={field_env_var_value}\n")
+
+ return app_settings
+
+
+settings = get_app_settings_from_env_file()
+
+if __name__ == "__main__":
+ logger.info(settings)
diff --git a/utilities/utils.py b/utilities/utils.py
index 4307b5d3..26a3bc82 100644
--- a/utilities/utils.py
+++ b/utilities/utils.py
@@ -1,8 +1,8 @@
__author__ = "Wren J. R. (uberfastman)"
__email__ = "uberfastman@uberfastman.dev"
-from report.logger import get_logger
-from utilities.config import AppConfigParser
+from utilities.logger import get_logger
+from utilities.settings import settings
logger = get_logger(__name__, propagate=False)
@@ -10,10 +10,11 @@
def format_platform_display(platform: str) -> str:
return platform.capitalize() if len(platform) > 4 else platform.upper()
-def truncate_cell_for_display(config: AppConfigParser, cell_text: str, halve_max_chars: bool = False) -> str:
- max_chars: int = config.getint("Report", "max_data_chars", fallback=24)
+
+def truncate_cell_for_display(cell_text: str, halve_max_chars: bool = False) -> str:
+ max_chars: int = settings.report_settings.max_data_chars
if halve_max_chars:
max_chars //= 2
- return f"{cell_text[:max_chars].strip()}..." if len(cell_text) > max_chars else cell_text
+ return f"{cell_text[:max_chars].strip()}..." if len(cell_text) > max_chars else cell_text