├── .env.example ├── .flake8 ├── .github ├── FUNDING.yml ├── flask-assets@2x.jpg └── workflows │ └── pythonapp.yml ├── .gitignore ├── .nvm ├── LICENSE ├── Makefile ├── README.md ├── config.py ├── flask_assets_tutorial ├── __init__.py ├── admin │ ├── routes.py │ ├── static │ │ └── admin.less │ └── templates │ │ └── dashboard.jinja2 ├── assets.py ├── main │ ├── routes.py │ ├── static │ │ └── homepage.less │ └── templates │ │ └── index.jinja2 ├── static │ ├── dist │ │ ├── css │ │ │ ├── account.css │ │ │ └── landing.css │ │ ├── img │ │ │ ├── favicon@2x.png │ │ │ └── logo@2x.png │ │ └── js │ │ │ └── main.min.js │ └── src │ │ ├── js │ │ └── main.js │ │ └── less │ │ ├── global.less │ │ ├── nav.less │ │ └── variables.less └── templates │ ├── analytics.jinja2 │ ├── layout.jinja2 │ ├── meta.jinja2 │ ├── navigation-default.jinja2 │ └── navigation-loggedin.jinja2 ├── gunicorn.conf.py ├── log.py ├── poetry.lock ├── pyproject.toml ├── renovate.json ├── requirements.txt └── wsgi.py /.env.example: -------------------------------------------------------------------------------- 1 | FLASK_ENV=development 2 | SECRET_KEY="HGuitfI&uf6i7r&ujHFc" 3 | FLASK_DEBUG=False 4 | LESS_BIN="/Users/myuser/.nvm/versions/node/v18.18.1/bin/lessc" -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | select = E9,F63,F7,F82 3 | exclude = .git,.github,__pycache__,.pytest_cache,.venv,logs,creds 4 | max-line-length = 120 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: ['https://www.buymeacoffee.com/hackersslackers'] -------------------------------------------------------------------------------- /.github/flask-assets@2x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackersandslackers/flask-assets-tutorial/82fbaac579c9c7d19aa9383876644977b17bfc21/.github/flask-assets@2x.jpg -------------------------------------------------------------------------------- /.github/workflows/pythonapp.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python application 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-python@v5 19 | with: 20 | python-version: "3.10" 21 | cache: "pip" # caching pip dependencies 22 | 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install flake8 pytest 27 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 28 | 29 | - name: Lint with flake8 30 | run: | 31 | # stop the build if there are Python syntax errors or undefined names 32 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 33 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 34 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | downloads/ 14 | eggs/ 15 | .eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | MANIFEST 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a static 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | .hypothesis/ 47 | .pytest_cache/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | db.sqlite3 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # Environments 84 | .env 85 | .venv 86 | env/ 87 | venv/ 88 | ENV/ 89 | env.bak/ 90 | venv.bak/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | .spyproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # mypy 103 | .mypy_cache/ 104 | 105 | # DS Store 106 | .DS_Store 107 | 108 | # idea 109 | .vscode 110 | .idea 111 | -------------------------------------------------------------------------------- /.nvm: -------------------------------------------------------------------------------- 1 | 20.18.1 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Hackers and Slackers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT_NAME := $(shell basename $CURDIR) 2 | VIRTUAL_ENV := $(CURDIR)/.venv 3 | LOCAL_PYTHON := $(VIRTUAL_ENV)/bin/python3 4 | LOCAL_NODE_JS := $(shell which node) 5 | LOCAL_LESS_JS := $(shell which lessc) 6 | 7 | define HELP 8 | Manage $(PROJECT_NAME). Usage: 9 | 10 | make run - Run $(PROJECT_NAME) locally. 11 | make install - Create local virtualenv & install dependencies. 12 | make deploy - Set up project & run locally. 13 | make update - Update dependencies via Poetry and output resulting `requirements.txt`. 14 | make format - Run Python code formatter & sort dependencies. 15 | make lint - Check code formatting with flake8. 16 | make clean - Remove extraneous compiled files, caches, logs, etc. 17 | 18 | endef 19 | export HELP 20 | 21 | 22 | .PHONY: run install deploy update format lint clean help 23 | 24 | all help: 25 | @echo "$$HELP" 26 | 27 | env: $(VIRTUAL_ENV) 28 | node-env: $(LOCAL_NODE_JS) 29 | 30 | $(VIRTUAL_ENV): 31 | if [ ! -d $(VIRTUAL_ENV) ]; then \ 32 | echo "Creating Python virtual env in \`${VIRTUAL_ENV}\`"; \ 33 | python3 -m venv $(VIRTUAL_ENV); \ 34 | fi; 35 | 36 | $(LOCAL_NODE_JS): 37 | if [ ! -f $(LOCAL_LESS_JS) ]; then \ 38 | echo "Installing lessc"; \ 39 | npm install -g lessc; \ 40 | fi; 41 | 42 | .PHONY: run 43 | run: env 44 | $(LOCAL_PYTHON) -m gunicorn --config=gunicorn.conf.py 45 | 46 | .PHONY: install 47 | install: env 48 | $(LOCAL_PYTHON) -m pip install --upgrade pip setuptools wheel && \ 49 | $(LOCAL_PYTHON) -m pip install -r requirements.txt && \ 50 | npm i -g less && \ 51 | echo Installed dependencies in \`${VIRTUAL_ENV}\`; 52 | 53 | .PHONY: deploy 54 | deploy: 55 | make install && \ 56 | make run 57 | 58 | .PHONY: test 59 | test: env 60 | $(LOCAL_PYTHON) -m \ 61 | coverage run -m pytest -vv \ 62 | --disable-pytest-warnings && \ 63 | coverage html --title='Coverage Report' -d .reports && \ 64 | open .reports/index.html 65 | 66 | .PHONY: update 67 | update: env 68 | $(LOCAL_PYTHON) -m pip install --upgrade pip setuptools wheel && \ 69 | poetry update && \ 70 | poetry export -f requirements.txt --output requirements.txt --without-hashes && \ 71 | echo Installed dependencies in \`${VIRTUAL_ENV}\`; 72 | 73 | .PHONY: format 74 | format: env 75 | $(LOCAL_PYTHON) -m isort --multi-line=3 . && \ 76 | $(LOCAL_PYTHON) -m black . 77 | 78 | .PHONY: lint 79 | lint: env 80 | $(LOCAL_PYTHON) -m flake8 . --count \ 81 | --select=E9,F63,F7,F82 \ 82 | --exclude .git,.github,__pycache__,.pytest_cache,.venv,logs,creds,.venv,docs,logs,.reports \ 83 | --show-source \ 84 | --statistics 85 | 86 | 87 | .PHONY: clean 88 | clean: 89 | find . -name '.coverage' -delete && \ 90 | find . -name '*.pyc' -delete && \ 91 | find . -name '__pycache__' -delete && \ 92 | find . -name 'poetry.lock' -delete && \ 93 | find . -name '*.log' -delete && \ 94 | find . -name '.DS_Store' -delete && \ 95 | find . -name 'Pipfile' -delete && \ 96 | find . -name 'Pipfile.lock' -delete && \ 97 | find . -wholename '**/*.pyc' -delete && \ 98 | find . -wholename '**/*.html' -delete && \ 99 | find . -type d -wholename '__pycache__' -exec rm -rf {} + && \ 100 | find . -type d -wholename '.venv' -exec rm -rf {} + && \ 101 | find . -type d -wholename '.pytest_cache' -exec rm -rf {} + && \ 102 | find . -type d -wholename '**/.pytest_cache' -exec rm -rf {} + && \ 103 | find . -type d -wholename '**/*.log' -exec rm -rf {} + && \ 104 | find . -type d -wholename './.reports/*' -exec rm -rf {} + && \ 105 | find . -type d -wholename '**/.webassets-cache' -exec rm -rf {} + -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask-Assets Tutorial 2 | 3 | ![Python](https://img.shields.io/badge/Python-v3.12-blue.svg?logo=python&longCache=true&logoColor=white&colorB=5e81ac&style=flat-square&colorA=4c566a) 4 | ![Flask](https://img.shields.io/badge/Flask-v3.0.0-blue.svg?longCache=true&logo=flask&style=flat-square&logoColor=white&colorB=5e81ac&colorA=4c566a) 5 | ![Flask-Assets](https://img.shields.io/badge/Flask--Assets-v2.1.0-blue.svg?longCache=true&logo=flask&style=flat-square&logoColor=white&colorB=5e81ac&colorA=4c566a) 6 | ![Gunicorn](https://img.shields.io/badge/Gunicorn-v21.2.0-blue.svg?longCache=true&logo=gunicorn&style=flat-square&logoColor=white&colorB=a3be8c&colorA=4c566a) 7 | ![GitHub Last Commit](https://img.shields.io/github/last-commit/google/skia.svg?style=flat-square&colorA=4c566a&colorB=a3be8c&logo=GitHub) 8 | [![GitHub Issues](https://img.shields.io/github/issues/hackersandslackers/flask-assets-tutorial.svg?style=flat-square&colorA=4c566a&logo=GitHub&colorB=ebcb8b)](https://github.com/hackersandslackers/flask-assets-tutorial/issues) 9 | [![GitHub Stars](https://img.shields.io/github/stars/hackersandslackers/flask-assets-tutorial.svg?style=flat-square&colorA=4c566a&logo=GitHub&colorB=ebcb8b)](https://github.com/hackersandslackers/flask-assets-tutorial/stargazers) 10 | [![GitHub Forks](https://img.shields.io/github/forks/hackersandslackers/flask-assets-tutorial.svg?style=flat-square&colorA=4c566a&logo=GitHub&colorB=ebcb8b)](https://github.com/hackersandslackers/flask-assets-tutorial/network) 11 | 12 | ![Flask Assets Tutorial](https://github.com/hackersandslackers/flask-assets-tutorial/blob/master/.github/flask-assets@2x.jpg?raw=true) 13 | 14 | Build and code-split your frontend assets across Blueprints using Flask-Assets. 15 | 16 | * **Tutorial**: [https://hackersandslackers.com/flask-assets/](https://hackersandslackers.com/flask-assets/) 17 | * **Demo**: [https://flaskassets.hackersandslackers.app/](https://flaskassets.hackersandslackers.app/) 18 | 19 | ## Getting Started 20 | 21 | Get set up locally in two steps: 22 | 23 | ### Environment Variables 24 | 25 | Replace the values in **.env.example** with your values and rename this file to **.env**: 26 | 27 | * `ENVIRONMENT`: The environment in which to run your application (either `development` or `production`). 28 | * `FLASK_DEBUG`: Set to `True` to enable Flask's debug mode (default to `False` in prod). 29 | * `SECRET_KEY`: Randomly generated string of characters used to encrypt your app's data. 30 | * `LESS_BIN`: Path to your local LESS installation via `which lessc`. 31 | 32 | *Remember never to commit secrets saved in .env files to Github.* 33 | 34 | ### Installation 35 | 36 | Get up and running with `make deploy`: 37 | 38 | ```shell 39 | git clone https://github.com/hackersandslackers/flask-assets-tutorial.git 40 | cd flask-assets-tutorial 41 | make deploy 42 | ``` 43 | 44 | ----- 45 | 46 | **Hackers and Slackers** tutorials are free of charge. If you found this tutorial helpful, a [small donation](https://www.buymeacoffee.com/hackersslackers) would be greatly appreciated to keep us in business. All proceeds go towards coffee, and all coffee goes towards more content. 47 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | """Flask configuration variables.""" 2 | 3 | from os import environ, path 4 | 5 | from dotenv import load_dotenv 6 | 7 | basedir = path.abspath(path.dirname(__file__)) 8 | load_dotenv(path.join(basedir, ".env")) 9 | 10 | 11 | class Config: 12 | """Set Flask configuration from .env file.""" 13 | 14 | # General Config 15 | APPLICATION_NAME = "flaskassets" 16 | ENVIRONMENT = environ.get("ENVIRONMENT") 17 | 18 | # Flask Config 19 | FLASK_APP = "wsgi.py" 20 | SECRET_KEY = environ.get("SECRET_KEY") 21 | FLASK_DEBUG = environ.get("FLASK_DEBUG") 22 | 23 | # Flask-Assets 24 | LESS_BIN = environ.get("LESS_BIN") 25 | ASSETS_DEBUG = True 26 | LESS_RUN_IN_DEBUG = True 27 | 28 | # Static Assets 29 | STATIC_FOLDER = "static" 30 | TEMPLATES_FOLDER = "templates" 31 | COMPRESSOR_DEBUG = True 32 | 33 | 34 | settings = Config() 35 | -------------------------------------------------------------------------------- /flask_assets_tutorial/__init__.py: -------------------------------------------------------------------------------- 1 | """Initialize Flask app.""" 2 | 3 | from flask import Flask 4 | from flask_assets import Environment 5 | 6 | 7 | def create_app(): 8 | """Construct core Flask app.""" 9 | app = Flask(__name__, instance_relative_config=False) 10 | app.config.from_object("config.Config") 11 | assets = Environment() 12 | 13 | # Initialize plugins 14 | assets.init_app(app) 15 | 16 | with app.app_context(): 17 | # Import parts of our flask_assets_tutorial 18 | from .admin import routes as admin_routes 19 | from .assets import compile_js_assets, compile_stylesheet_bundles 20 | from .main import routes as main_routes 21 | 22 | # Register Blueprints 23 | app.register_blueprint(admin_routes.admin_blueprint) 24 | app.register_blueprint(main_routes.main_blueprint) 25 | 26 | # Compile static assets 27 | compile_stylesheet_bundles(assets) 28 | compile_js_assets(assets) 29 | 30 | return app 31 | -------------------------------------------------------------------------------- /flask_assets_tutorial/admin/routes.py: -------------------------------------------------------------------------------- 1 | """Routes for logged-in account pages.""" 2 | 3 | from flask import Blueprint, render_template 4 | 5 | from log import LOGGER 6 | 7 | admin_blueprint = Blueprint("admin_blueprint", __name__, template_folder="templates", static_folder="static") 8 | 9 | 10 | @admin_blueprint.route("/dashboard", methods=["GET"]) 11 | def dashboard(): 12 | """Admin dashboard route.""" 13 | LOGGER.info("Admin dashboard rendered by user.") 14 | return render_template( 15 | "dashboard.jinja2", 16 | title="Admin Dashboard", 17 | template="dashboard-static account", 18 | body="This is the admin dashboard.", 19 | logged_in=True, 20 | ) 21 | -------------------------------------------------------------------------------- /flask_assets_tutorial/admin/static/admin.less: -------------------------------------------------------------------------------- 1 | p { 2 | width: 100% 3 | } -------------------------------------------------------------------------------- /flask_assets_tutorial/admin/templates/dashboard.jinja2: -------------------------------------------------------------------------------- 1 | {% extends "layout.jinja2" %} 2 | 3 | {% block pagestyles %} 4 | 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

{{title}}

9 |

{{body}}

10 | {% endblock %} -------------------------------------------------------------------------------- /flask_assets_tutorial/assets.py: -------------------------------------------------------------------------------- 1 | """Compile static assets.""" 2 | 3 | from flask import current_app as app 4 | from flask_assets import Bundle, Environment 5 | 6 | 7 | def compile_stylesheet_bundles(assets: Environment) -> Environment: 8 | """ 9 | Create minified CSS bundles from LESS styles. 10 | 11 | :param Environment assets: Flask `environment` for static assets. 12 | 13 | :returns: Environment 14 | """ 15 | # Main Stylesheets Bundle 16 | main_style_bundle = Bundle( 17 | "src/less/*.less", 18 | "main_blueprint/homepage.less", 19 | filters="less,cssmin", 20 | output="dist/css/landing.css", 21 | extra={"rel": "stylesheet/css"}, 22 | ) 23 | # Admin Stylesheets Bundle 24 | admin_style_bundle = Bundle( 25 | "src/less/*.less", 26 | "admin_blueprint/admin.less", 27 | filters="less,cssmin", 28 | output="dist/css/account.css", 29 | extra={"rel": "stylesheet/css"}, 30 | ) 31 | assets.register("main_styles", main_style_bundle) 32 | assets.register("admin_styles", admin_style_bundle) 33 | if app.config["ENVIRONMENT"] == "development": 34 | main_style_bundle.build() 35 | admin_style_bundle.build() 36 | return assets 37 | 38 | 39 | def compile_js_assets(assets: Environment) -> Environment: 40 | """ 41 | Create minified JS bundles from raw Javascript files. 42 | 43 | :param Environment assets: Flask `environment` for static assets. 44 | 45 | :returns: Environment 46 | """ 47 | main_js_bundle = Bundle("src/js/main.js", filters="jsmin", output="dist/js/main.min.js") # Main JavaScript Bundle 48 | assets.register("main_js", main_js_bundle) 49 | if app.config["ENVIRONMENT"] == "development": 50 | main_js_bundle.build() 51 | return assets 52 | -------------------------------------------------------------------------------- /flask_assets_tutorial/main/routes.py: -------------------------------------------------------------------------------- 1 | """Routes for main pages.""" 2 | 3 | from flask import Blueprint, render_template 4 | 5 | from log import LOGGER 6 | 7 | main_blueprint = Blueprint("main_blueprint", __name__, template_folder="templates", static_folder="static") 8 | 9 | 10 | @main_blueprint.route("/", methods=["GET"]) 11 | def home(): 12 | """Home page route.""" 13 | LOGGER.info("Home page rendered by user.") 14 | return render_template( 15 | "index.jinja2", 16 | title="Home", 17 | body="Welcome to the Flask-Assets tutorial demo!", 18 | template="home-static main", 19 | logged_in=False, 20 | ) 21 | 22 | 23 | @main_blueprint.route("/about", methods=["GET"]) 24 | def about(): 25 | """About page route.""" 26 | LOGGER.info("About page rendered by user.") 27 | return render_template( 28 | "index.jinja2", 29 | title="About", 30 | body="At HackersAndSlackers, we pride ourselves in stuff. That's why work tirelessly to build and deliver stuff, 24/7.", 31 | template="about-static main", 32 | logged_in=False, 33 | ) 34 | 35 | 36 | @main_blueprint.route("/etc", methods=["GET"]) 37 | def etc(): 38 | """Etc page route.""" 39 | LOGGER.info("Etc. page rendered by user.") 40 | return render_template( 41 | "index.jinja2", 42 | title="Etc.", 43 | body="Here's a third page, for good measure.", 44 | template="etc-static main", 45 | logged_in=False, 46 | ) 47 | -------------------------------------------------------------------------------- /flask_assets_tutorial/main/static/homepage.less: -------------------------------------------------------------------------------- 1 | p { 2 | width: 100% 3 | } -------------------------------------------------------------------------------- /flask_assets_tutorial/main/templates/index.jinja2: -------------------------------------------------------------------------------- 1 | {% extends "layout.jinja2" %} 2 | 3 | {% block pagestyles %} 4 | 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

{{title}}

9 |

{{body}}

10 | 16 | {% endblock %} 17 | 18 | 19 | -------------------------------------------------------------------------------- /flask_assets_tutorial/static/dist/css/account.css: -------------------------------------------------------------------------------- 1 | /* Breakpoints */ 2 | nav { 3 | background: #fff; 4 | padding: 25px 0; 5 | margin: 0 auto; 6 | box-shadow: 0 0 5px #bec6cf; 7 | } 8 | @media (max-width: 600px) { 9 | nav { 10 | box-shadow: unset; 11 | } 12 | } 13 | nav .nav-wrapper { 14 | width: 915px; 15 | max-width: 80vw; 16 | display: flex; 17 | justify-content: space-between; 18 | align-items: center; 19 | margin: 0 auto; 20 | } 21 | @media (max-width: 800px) { 22 | nav .nav-wrapper { 23 | max-width: 90vw; 24 | } 25 | } 26 | nav .nav-wrapper .left-nav { 27 | display: flex; 28 | justify-content: space-between; 29 | align-items: center; 30 | } 31 | nav .nav-wrapper .left-nav .logo, 32 | nav .nav-wrapper .left-nav img { 33 | width: 40px; 34 | margin-right: 20px; 35 | } 36 | nav .nav-wrapper .left-nav a { 37 | margin-right: 20px; 38 | font-weight: 400; 39 | } 40 | nav a { 41 | color: #4d545d; 42 | transition: all 0.2s ease-out; 43 | text-decoration: none; 44 | } 45 | nav a:hover { 46 | cursor: pointer; 47 | opacity: 0.7; 48 | } 49 | body, 50 | html { 51 | font-family: 'Poppins', sans-serif; 52 | margin: 0; 53 | padding: 0; 54 | color: #2a2c2f; 55 | background: #f0f0f0; 56 | height: 100%; 57 | } 58 | body .container, 59 | html .container { 60 | width: 850px; 61 | max-width: 80vw; 62 | min-height: 100%; 63 | background: white; 64 | margin: 40px auto 0; 65 | padding: 40px; 66 | box-shadow: 0 0 5px #bec6cf; 67 | } 68 | @media (max-width: 800px) { 69 | body .container, 70 | html .container { 71 | width: 100%; 72 | } 73 | } 74 | @media (max-width: 600px) { 75 | body .container, 76 | html .container { 77 | width: 90vw; 78 | max-width: unset; 79 | padding: 5vw; 80 | margin: 0; 81 | box-shadow: 0 0 3px #bec6cf; 82 | } 83 | } 84 | body .container .attribute-value, 85 | html .container .attribute-value { 86 | font-weight: 300; 87 | font-size: 0.9em; 88 | } 89 | body .container h1, 90 | html .container h1 { 91 | margin-top: 0; 92 | font-size: 2.5em; 93 | line-height: 1; 94 | color: #393b40; 95 | } 96 | body .container p, 97 | html .container p { 98 | font-weight: 300; 99 | font-size: 1.1em; 100 | line-height: 1.4; 101 | } 102 | ul { 103 | list-style: none; 104 | width: 50%; 105 | border: 1px solid #d0d4d9; 106 | border-radius: 3px; 107 | padding: 10px; 108 | } 109 | @media (max-width: 800px) { 110 | ul { 111 | width: auto; 112 | } 113 | } 114 | ul li { 115 | display: flex; 116 | justify-content: space-between; 117 | align-items: center; 118 | border-bottom: 1px solid #ececec; 119 | padding-bottom: 10px; 120 | margin-bottom: 10px; 121 | } 122 | ul li:last-of-type { 123 | border-bottom: none; 124 | margin-bottom: 0; 125 | padding-bottom: 0; 126 | } 127 | 128 | /* Breakpoints */ 129 | nav { 130 | background: #fff; 131 | padding: 25px 0; 132 | margin: 0 auto; 133 | box-shadow: 0 0 5px #bec6cf; 134 | } 135 | @media (max-width: 600px) { 136 | nav { 137 | box-shadow: unset; 138 | } 139 | } 140 | nav .nav-wrapper { 141 | width: 915px; 142 | max-width: 85vw; 143 | display: flex; 144 | justify-content: space-between; 145 | align-items: center; 146 | margin: 0 auto; 147 | } 148 | @media (max-width: 800px) { 149 | nav .nav-wrapper { 150 | max-width: 90vw; 151 | } 152 | } 153 | nav .nav-wrapper .left-nav { 154 | display: flex; 155 | justify-content: space-between; 156 | align-items: center; 157 | } 158 | nav .nav-wrapper .left-nav .logo, 159 | nav .nav-wrapper .left-nav img { 160 | width: 40px; 161 | margin-right: 20px; 162 | } 163 | nav .nav-wrapper .left-nav a { 164 | margin-right: 20px; 165 | font-weight: 400; 166 | } 167 | nav a { 168 | color: #4d545d; 169 | transition: all 0.2s ease-out; 170 | text-decoration: none; 171 | } 172 | nav a:hover { 173 | cursor: pointer; 174 | opacity: 0.7; 175 | } 176 | 177 | /* Breakpoints */ 178 | 179 | p { 180 | width: 100%; 181 | } 182 | -------------------------------------------------------------------------------- /flask_assets_tutorial/static/dist/css/landing.css: -------------------------------------------------------------------------------- 1 | /* Breakpoints */ 2 | nav { 3 | background: #fff; 4 | padding: 25px 0; 5 | margin: 0 auto; 6 | box-shadow: 0 0 5px #bec6cf; 7 | } 8 | @media (max-width: 600px) { 9 | nav { 10 | box-shadow: unset; 11 | } 12 | } 13 | nav .nav-wrapper { 14 | width: 915px; 15 | max-width: 80vw; 16 | display: flex; 17 | justify-content: space-between; 18 | align-items: center; 19 | margin: 0 auto; 20 | } 21 | @media (max-width: 800px) { 22 | nav .nav-wrapper { 23 | max-width: 90vw; 24 | } 25 | } 26 | nav .nav-wrapper .left-nav { 27 | display: flex; 28 | justify-content: space-between; 29 | align-items: center; 30 | } 31 | nav .nav-wrapper .left-nav .logo, 32 | nav .nav-wrapper .left-nav img { 33 | width: 40px; 34 | margin-right: 20px; 35 | } 36 | nav .nav-wrapper .left-nav a { 37 | margin-right: 20px; 38 | font-weight: 400; 39 | } 40 | nav a { 41 | color: #4d545d; 42 | transition: all 0.2s ease-out; 43 | text-decoration: none; 44 | } 45 | nav a:hover { 46 | cursor: pointer; 47 | opacity: 0.7; 48 | } 49 | body, 50 | html { 51 | font-family: 'Poppins', sans-serif; 52 | margin: 0; 53 | padding: 0; 54 | color: #2a2c2f; 55 | background: #f0f0f0; 56 | height: 100%; 57 | } 58 | body .container, 59 | html .container { 60 | width: 850px; 61 | max-width: 80vw; 62 | min-height: 100%; 63 | background: white; 64 | margin: 40px auto 0; 65 | padding: 40px; 66 | box-shadow: 0 0 5px #bec6cf; 67 | } 68 | @media (max-width: 800px) { 69 | body .container, 70 | html .container { 71 | width: 100%; 72 | } 73 | } 74 | @media (max-width: 600px) { 75 | body .container, 76 | html .container { 77 | width: 90vw; 78 | max-width: unset; 79 | padding: 5vw; 80 | margin: 0; 81 | box-shadow: 0 0 3px #bec6cf; 82 | } 83 | } 84 | body .container .attribute-value, 85 | html .container .attribute-value { 86 | font-weight: 300; 87 | font-size: 0.9em; 88 | } 89 | body .container h1, 90 | html .container h1 { 91 | margin-top: 0; 92 | font-size: 2.5em; 93 | line-height: 1; 94 | color: #393b40; 95 | } 96 | body .container p, 97 | html .container p { 98 | font-weight: 300; 99 | font-size: 1.1em; 100 | line-height: 1.4; 101 | } 102 | ul { 103 | list-style: none; 104 | width: 50%; 105 | border: 1px solid #d0d4d9; 106 | border-radius: 3px; 107 | padding: 10px; 108 | } 109 | @media (max-width: 800px) { 110 | ul { 111 | width: auto; 112 | } 113 | } 114 | ul li { 115 | display: flex; 116 | justify-content: space-between; 117 | align-items: center; 118 | border-bottom: 1px solid #ececec; 119 | padding-bottom: 10px; 120 | margin-bottom: 10px; 121 | } 122 | ul li:last-of-type { 123 | border-bottom: none; 124 | margin-bottom: 0; 125 | padding-bottom: 0; 126 | } 127 | 128 | /* Breakpoints */ 129 | nav { 130 | background: #fff; 131 | padding: 25px 0; 132 | margin: 0 auto; 133 | box-shadow: 0 0 5px #bec6cf; 134 | } 135 | @media (max-width: 600px) { 136 | nav { 137 | box-shadow: unset; 138 | } 139 | } 140 | nav .nav-wrapper { 141 | width: 915px; 142 | max-width: 85vw; 143 | display: flex; 144 | justify-content: space-between; 145 | align-items: center; 146 | margin: 0 auto; 147 | } 148 | @media (max-width: 800px) { 149 | nav .nav-wrapper { 150 | max-width: 90vw; 151 | } 152 | } 153 | nav .nav-wrapper .left-nav { 154 | display: flex; 155 | justify-content: space-between; 156 | align-items: center; 157 | } 158 | nav .nav-wrapper .left-nav .logo, 159 | nav .nav-wrapper .left-nav img { 160 | width: 40px; 161 | margin-right: 20px; 162 | } 163 | nav .nav-wrapper .left-nav a { 164 | margin-right: 20px; 165 | font-weight: 400; 166 | } 167 | nav a { 168 | color: #4d545d; 169 | transition: all 0.2s ease-out; 170 | text-decoration: none; 171 | } 172 | nav a:hover { 173 | cursor: pointer; 174 | opacity: 0.7; 175 | } 176 | 177 | /* Breakpoints */ 178 | 179 | p { 180 | width: 100%; 181 | } 182 | -------------------------------------------------------------------------------- /flask_assets_tutorial/static/dist/img/favicon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackersandslackers/flask-assets-tutorial/82fbaac579c9c7d19aa9383876644977b17bfc21/flask_assets_tutorial/static/dist/img/favicon@2x.png -------------------------------------------------------------------------------- /flask_assets_tutorial/static/dist/img/logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackersandslackers/flask-assets-tutorial/82fbaac579c9c7d19aa9383876644977b17bfc21/flask_assets_tutorial/static/dist/img/logo@2x.png -------------------------------------------------------------------------------- /flask_assets_tutorial/static/dist/js/main.min.js: -------------------------------------------------------------------------------- 1 | let alertButton=document.querySelector(".alert button");if(alertButton){alertButton.addEventListener("click",function(event){if(!event.target===alertButton)return;alertButton.parentNode.style.display="none";event.preventDefault();},false);} -------------------------------------------------------------------------------- /flask_assets_tutorial/static/src/js/main.js: -------------------------------------------------------------------------------- 1 | // Remove Alert on Close 2 | 3 | let alertButton = document.querySelector(".alert button"); 4 | 5 | if (alertButton) { 6 | alertButton.addEventListener( 7 | "click", 8 | function (event) { 9 | // If the clicked element doesn't have the right selector, bail 10 | if (!event.target === alertButton) return; 11 | alertButton.parentNode.style.display = "none"; 12 | 13 | // Don't follow the link 14 | event.preventDefault(); 15 | }, 16 | false 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /flask_assets_tutorial/static/src/less/global.less: -------------------------------------------------------------------------------- 1 | @import 'variables.less'; 2 | @import 'nav.less'; 3 | 4 | body, 5 | html { 6 | font-family: @body-font; 7 | margin: 0; 8 | padding: 0; 9 | color: #2a2c2f; 10 | background: #f0f0f0; 11 | height: 100%; 12 | 13 | .container { 14 | width: 850px; 15 | max-width: 80vw; 16 | min-height: 100%; 17 | background: white; 18 | margin: 40px auto 0; 19 | padding: 40px; 20 | box-shadow: 0 0 5px #bec6cf; 21 | 22 | @media(max-width: @tablet-breakpoint) { 23 | width: 100%; 24 | } 25 | 26 | @media(max-width: @mobile-breakpoint) { 27 | width: 90vw; 28 | max-width: unset; 29 | padding: 5vw; 30 | margin: 0; 31 | box-shadow: 0 0 3px #bec6cf; 32 | } 33 | 34 | .attribute-value { 35 | font-weight: 300; 36 | font-size: .9em; 37 | } 38 | 39 | h1 { 40 | margin-top: 0; 41 | font-size: 2.5em; 42 | line-height: 1; 43 | color: @title-color; 44 | } 45 | 46 | p { 47 | font-weight: 300; 48 | font-size: 1.1em; 49 | line-height: 1.4; 50 | } 51 | } 52 | } 53 | 54 | ul { 55 | list-style: none; 56 | width: 50%; 57 | border: 1px solid #d0d4d9; 58 | border-radius: 3px; 59 | padding: 10px; 60 | @media(max-width: @tablet-breakpoint) { 61 | width: auto; 62 | } 63 | 64 | li { 65 | display: flex; 66 | justify-content: space-between; 67 | align-items: center; 68 | border-bottom: 1px solid #ececec; 69 | padding-bottom: 10px; 70 | margin-bottom: 10px; 71 | 72 | &:last-of-type { 73 | border-bottom: none; 74 | margin-bottom: 0; 75 | padding-bottom: 0; 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /flask_assets_tutorial/static/src/less/nav.less: -------------------------------------------------------------------------------- 1 | @import 'variables.less'; 2 | 3 | nav { 4 | background: #fff; 5 | padding: 25px 0; 6 | margin: 0 auto; 7 | box-shadow: 0 0 5px @box-shadow-nav; 8 | 9 | @media(max-width: @mobile-breakpoint) { 10 | box-shadow: unset; 11 | } 12 | 13 | .nav-wrapper { 14 | width: 915px; 15 | max-width: 85vw; 16 | display: flex; 17 | justify-content: space-between; 18 | align-items: center; 19 | margin: 0 auto; 20 | @media(max-width: @tablet-breakpoint) { 21 | max-width: 90vw; 22 | } 23 | 24 | .left-nav { 25 | display: flex; 26 | justify-content: space-between; 27 | align-items: center; 28 | 29 | .logo, 30 | img { 31 | width: 40px; 32 | margin-right: 20px; 33 | } 34 | 35 | a { 36 | margin-right: 20px; 37 | font-weight: 400; 38 | } 39 | } 40 | } 41 | 42 | a { 43 | color: @nav-link-color; 44 | transition: @transition; 45 | text-decoration: none; 46 | 47 | &:hover { 48 | cursor: pointer; 49 | opacity: .7; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /flask_assets_tutorial/static/src/less/variables.less: -------------------------------------------------------------------------------- 1 | // Colors 2 | @theme-color: #5eb9d7; 3 | @background-color: #e1eaf5; 4 | @box-shadow: 0 0 5px rgba(65, 67, 144, 0.15); 5 | @box-shadow-nav: #bec6cf; 6 | @nav-link-color: #4d545d; 7 | @header-color: #5f6988; 8 | @title-color: #393b40; 9 | 10 | // Fonts 11 | @body-font: 'Poppins', sans-serif; 12 | 13 | // Break points 14 | /* Breakpoints */ 15 | @smallscreen-breakpoint: 950px; 16 | @tablet-breakpoint: 800px; 17 | @mobile-breakpoint: 600px; 18 | 19 | // Animation 20 | @transition: all .2s ease-out; 21 | -------------------------------------------------------------------------------- /flask_assets_tutorial/templates/analytics.jinja2: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /flask_assets_tutorial/templates/layout.jinja2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% include 'meta.jinja2' %} 6 | {% block pagestyles %}{% endblock %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% if logged_in %} 14 | {% include "navigation-loggedin.jinja2" %} 15 | {% else %} 16 | {% include "navigation-default.jinja2" %} 17 | {% endif %} 18 | 19 |
20 | {% block content %}{% endblock %} 21 |
22 | 23 | {# Scripts #} 24 | 25 | {% block additionalscripts %}{% endblock %} 26 | {% include 'analytics.jinja2' %} 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /flask_assets_tutorial/templates/meta.jinja2: -------------------------------------------------------------------------------- 1 | {% block meta %} 2 | 3 | 4 | {{title}} | Flask-Assets Tutorial 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /flask_assets_tutorial/templates/navigation-default.jinja2: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /flask_assets_tutorial/templates/navigation-loggedin.jinja2: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /gunicorn.conf.py: -------------------------------------------------------------------------------- 1 | """Gunicorn configuration file.""" 2 | 3 | from os import environ, path 4 | 5 | from dotenv import load_dotenv 6 | 7 | basedir = path.abspath(path.dirname(__file__)) 8 | load_dotenv(path.join(basedir, ".env")) 9 | 10 | ENVIRONMENT = environ.get("ENVIRONMENT") 11 | 12 | proc_name = "flaskassets" 13 | wsgi_app = "wsgi:app" 14 | bind = "unix:flask.sock" 15 | threads = 4 16 | workers = 2 17 | 18 | if ENVIRONMENT == "development": 19 | reload = True 20 | workers = 1 21 | threads = 1 22 | bind = ["127.0.0.1:8000"] 23 | 24 | if ENVIRONMENT == "production": 25 | daemon = True 26 | accesslog = "/var/log/flaskassets/info.log" 27 | errorlog = "/var/log/flaskassets/error.log" 28 | loglevel = "trace" 29 | dogstatsd_tags = "env:prod,service:flaskassets,language:python,type:tutorial" 30 | -------------------------------------------------------------------------------- /log.py: -------------------------------------------------------------------------------- 1 | """Custom logger.""" 2 | 3 | import json 4 | from sys import stdout 5 | 6 | from loguru import logger 7 | 8 | from config import settings 9 | 10 | 11 | def json_formatter(record: dict) -> str: 12 | """ 13 | Pass raw log to be serialized. 14 | 15 | :param dict record: Dictionary containing logged message with metadata. 16 | 17 | :returns: str 18 | """ 19 | 20 | def serialize(log: dict) -> str: 21 | """ 22 | Parse log message into Datadog JSON format. 23 | 24 | :param dict log: Dictionary containing logged message with metadata. 25 | 26 | :returns: str 27 | """ 28 | subset = { 29 | "time": log["time"].strftime("%m/%d/%Y, %H:%M:%S"), 30 | "message": log["message"], 31 | "level": log["level"].name, 32 | "function": log.get("function"), 33 | "module": log.get("name"), 34 | } 35 | if log.get("exception", None): 36 | subset.update({"exception": log["exception"]}) 37 | return json.dumps(subset) 38 | 39 | record["extra"]["serialized"] = serialize(record) 40 | return "{extra[serialized]},\n" 41 | 42 | 43 | def log_formatter(record: dict) -> str: 44 | """ 45 | Formatter for .log records 46 | 47 | :param dict record: Key/value object containing log message & metadata. 48 | 49 | :returns: str 50 | """ 51 | if record["level"].name == "TRACE": 52 | return "{time:MM-DD-YYYY HH:mm:ss} | {level}: {message}\n" 53 | if record["level"].name == "INFO": 54 | return "{time:MM-DD-YYYY HH:mm:ss} | {level}: {message}\n" 55 | if record["level"].name == "WARNING": 56 | return "{time:MM-DD-YYYY HH:mm:ss} | {level}: {message}\n" 57 | if record["level"].name == "SUCCESS": 58 | return "{time:MM-DD-YYYY HH:mm:ss} | {level}: {message}\n" 59 | if record["level"].name == "ERROR": 60 | return "{time:MM-DD-YYYY HH:mm:ss} | {level}: {message}\n" 61 | if record["level"].name == "CRITICAL": 62 | return "{time:MM-DD-YYYY HH:mm:ss} | {level}: {message}\n" 63 | return "{time:MM-DD-YYYY HH:mm:ss} | {level}: {message}\n" 64 | 65 | 66 | def create_logger() -> logger: 67 | """ 68 | Configure custom logger. 69 | 70 | :returns: logger 71 | """ 72 | logger.remove() 73 | logger.add( 74 | stdout, 75 | colorize=True, 76 | catch=True, 77 | level="TRACE", 78 | format=log_formatter, 79 | ) 80 | if settings.ENVIRONMENT == "production": 81 | # Datadog JSON logs 82 | logger.add( 83 | f"/var/log/{settings.APPLICATION_NAME}/info.json", 84 | format=json_formatter, 85 | rotation="200 MB", 86 | level="TRACE", 87 | compression="zip", 88 | ) 89 | # Readable logs 90 | logger.add( 91 | f"/var/log/{settings.APPLICATION_NAME}/info.log", 92 | colorize=True, 93 | catch=True, 94 | level="TRACE", 95 | format=log_formatter, 96 | rotation="200 MB", 97 | compression="zip", 98 | ) 99 | else: 100 | logger.add( 101 | "./logs/info.log", 102 | colorize=True, 103 | catch=True, 104 | format=log_formatter, 105 | rotation="200 MB", 106 | compression="zip", 107 | level="INFO", 108 | ) 109 | logger.add( 110 | "./logs/error.log", 111 | colorize=True, 112 | catch=True, 113 | format=log_formatter, 114 | rotation="200 MB", 115 | compression="zip", 116 | level="ERROR", 117 | ) 118 | return logger 119 | 120 | 121 | # Custom logger 122 | LOGGER = create_logger() 123 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "black" 5 | version = "25.1.0" 6 | description = "The uncompromising code formatter." 7 | optional = false 8 | python-versions = ">=3.9" 9 | groups = ["main"] 10 | files = [ 11 | {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"}, 12 | {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"}, 13 | {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"}, 14 | {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"}, 15 | {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"}, 16 | {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"}, 17 | {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"}, 18 | {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"}, 19 | {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"}, 20 | {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"}, 21 | {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"}, 22 | {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"}, 23 | {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"}, 24 | {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"}, 25 | {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"}, 26 | {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"}, 27 | {file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"}, 28 | {file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"}, 29 | {file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"}, 30 | {file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"}, 31 | {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"}, 32 | {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"}, 33 | ] 34 | 35 | [package.dependencies] 36 | click = ">=8.0.0" 37 | mypy-extensions = ">=0.4.3" 38 | packaging = ">=22.0" 39 | pathspec = ">=0.9.0" 40 | platformdirs = ">=2" 41 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 42 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 43 | 44 | [package.extras] 45 | colorama = ["colorama (>=0.4.3)"] 46 | d = ["aiohttp (>=3.10)"] 47 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 48 | uvloop = ["uvloop (>=0.15.2)"] 49 | 50 | [[package]] 51 | name = "blinker" 52 | version = "1.9.0" 53 | description = "Fast, simple object-to-object and broadcast signaling" 54 | optional = false 55 | python-versions = ">=3.9" 56 | groups = ["main"] 57 | files = [ 58 | {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, 59 | {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, 60 | ] 61 | 62 | [[package]] 63 | name = "click" 64 | version = "8.1.8" 65 | description = "Composable command line interface toolkit" 66 | optional = false 67 | python-versions = ">=3.7" 68 | groups = ["main"] 69 | files = [ 70 | {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, 71 | {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, 72 | ] 73 | 74 | [package.dependencies] 75 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 76 | 77 | [[package]] 78 | name = "colorama" 79 | version = "0.4.6" 80 | description = "Cross-platform colored terminal text." 81 | optional = false 82 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 83 | groups = ["main"] 84 | markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" 85 | files = [ 86 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 87 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 88 | ] 89 | 90 | [[package]] 91 | name = "cssmin" 92 | version = "0.2.0" 93 | description = "A Python port of the YUI CSS compression algorithm." 94 | optional = false 95 | python-versions = "*" 96 | groups = ["main"] 97 | files = [ 98 | {file = "cssmin-0.2.0.tar.gz", hash = "sha256:e012f0cc8401efcf2620332339011564738ae32be8c84b2e43ce8beaec1067b6"}, 99 | ] 100 | 101 | [[package]] 102 | name = "exceptiongroup" 103 | version = "1.2.2" 104 | description = "Backport of PEP 654 (exception groups)" 105 | optional = false 106 | python-versions = ">=3.7" 107 | groups = ["main"] 108 | markers = "python_version < \"3.11\"" 109 | files = [ 110 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 111 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 112 | ] 113 | 114 | [package.extras] 115 | test = ["pytest (>=6)"] 116 | 117 | [[package]] 118 | name = "flake8" 119 | version = "7.1.1" 120 | description = "the modular source code checker: pep8 pyflakes and co" 121 | optional = false 122 | python-versions = ">=3.8.1" 123 | groups = ["main"] 124 | files = [ 125 | {file = "flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"}, 126 | {file = "flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38"}, 127 | ] 128 | 129 | [package.dependencies] 130 | mccabe = ">=0.7.0,<0.8.0" 131 | pycodestyle = ">=2.12.0,<2.13.0" 132 | pyflakes = ">=3.2.0,<3.3.0" 133 | 134 | [[package]] 135 | name = "flask" 136 | version = "3.1.0" 137 | description = "A simple framework for building complex web applications." 138 | optional = false 139 | python-versions = ">=3.9" 140 | groups = ["main"] 141 | files = [ 142 | {file = "flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136"}, 143 | {file = "flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac"}, 144 | ] 145 | 146 | [package.dependencies] 147 | blinker = ">=1.9" 148 | click = ">=8.1.3" 149 | itsdangerous = ">=2.2" 150 | Jinja2 = ">=3.1.2" 151 | Werkzeug = ">=3.1" 152 | 153 | [package.extras] 154 | async = ["asgiref (>=3.2)"] 155 | dotenv = ["python-dotenv"] 156 | 157 | [[package]] 158 | name = "flask-assets" 159 | version = "2.1.0" 160 | description = "Asset management for Flask, to compress and merge CSS and Javascript files." 161 | optional = false 162 | python-versions = "*" 163 | groups = ["main"] 164 | files = [ 165 | {file = "Flask-Assets-2.1.0.tar.gz", hash = "sha256:f84d6532ffe59c9ff352885e8740ff4da25c0bcfacd805f0a806815e44354813"}, 166 | {file = "Flask_Assets-2.1.0-py3-none-any.whl", hash = "sha256:a56c476b15f84701712cc3b4b4a001ebbe62b1dcbe81c23f96fbe6f261b75324"}, 167 | ] 168 | 169 | [package.dependencies] 170 | Flask = ">=0.8" 171 | webassets = ">=2.0" 172 | 173 | [[package]] 174 | name = "flask-sock" 175 | version = "0.7.0" 176 | description = "WebSocket support for Flask" 177 | optional = false 178 | python-versions = ">=3.6" 179 | groups = ["main"] 180 | files = [ 181 | {file = "flask-sock-0.7.0.tar.gz", hash = "sha256:e023b578284195a443b8d8bdb4469e6a6acf694b89aeb51315b1a34fcf427b7d"}, 182 | {file = "flask_sock-0.7.0-py3-none-any.whl", hash = "sha256:caac4d679392aaf010d02fabcf73d52019f5bdaf1c9c131ec5a428cb3491204a"}, 183 | ] 184 | 185 | [package.dependencies] 186 | flask = ">=2" 187 | simple-websocket = ">=0.5.1" 188 | 189 | [package.extras] 190 | docs = ["sphinx"] 191 | 192 | [[package]] 193 | name = "gunicorn" 194 | version = "23.0.0" 195 | description = "WSGI HTTP Server for UNIX" 196 | optional = false 197 | python-versions = ">=3.7" 198 | groups = ["main"] 199 | files = [ 200 | {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, 201 | {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, 202 | ] 203 | 204 | [package.dependencies] 205 | packaging = "*" 206 | 207 | [package.extras] 208 | eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] 209 | gevent = ["gevent (>=1.4.0)"] 210 | setproctitle = ["setproctitle"] 211 | testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] 212 | tornado = ["tornado (>=0.2)"] 213 | 214 | [[package]] 215 | name = "h11" 216 | version = "0.14.0" 217 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 218 | optional = false 219 | python-versions = ">=3.7" 220 | groups = ["main"] 221 | files = [ 222 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 223 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 224 | ] 225 | 226 | [[package]] 227 | name = "iniconfig" 228 | version = "2.0.0" 229 | description = "brain-dead simple config-ini parsing" 230 | optional = false 231 | python-versions = ">=3.7" 232 | groups = ["main"] 233 | files = [ 234 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 235 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 236 | ] 237 | 238 | [[package]] 239 | name = "isort" 240 | version = "6.0.1" 241 | description = "A Python utility / library to sort Python imports." 242 | optional = false 243 | python-versions = ">=3.9.0" 244 | groups = ["main"] 245 | files = [ 246 | {file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"}, 247 | {file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"}, 248 | ] 249 | 250 | [package.extras] 251 | colors = ["colorama"] 252 | plugins = ["setuptools"] 253 | 254 | [[package]] 255 | name = "itsdangerous" 256 | version = "2.2.0" 257 | description = "Safely pass data to untrusted environments and back." 258 | optional = false 259 | python-versions = ">=3.8" 260 | groups = ["main"] 261 | files = [ 262 | {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, 263 | {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, 264 | ] 265 | 266 | [[package]] 267 | name = "jinja2" 268 | version = "3.1.6" 269 | description = "A very fast and expressive template engine." 270 | optional = false 271 | python-versions = ">=3.7" 272 | groups = ["main"] 273 | files = [ 274 | {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, 275 | {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, 276 | ] 277 | 278 | [package.dependencies] 279 | MarkupSafe = ">=2.0" 280 | 281 | [package.extras] 282 | i18n = ["Babel (>=2.7)"] 283 | 284 | [[package]] 285 | name = "jsmin" 286 | version = "3.0.1" 287 | description = "JavaScript minifier." 288 | optional = false 289 | python-versions = "*" 290 | groups = ["main"] 291 | files = [ 292 | {file = "jsmin-3.0.1.tar.gz", hash = "sha256:c0959a121ef94542e807a674142606f7e90214a2b3d1eb17300244bbb5cc2bfc"}, 293 | ] 294 | 295 | [[package]] 296 | name = "lesscpy" 297 | version = "0.15.1" 298 | description = "Python LESS compiler" 299 | optional = false 300 | python-versions = "*" 301 | groups = ["main"] 302 | files = [ 303 | {file = "lesscpy-0.15.1-py2.py3-none-any.whl", hash = "sha256:8d26e58ed4812b345c2896daea435a28cb3182f87ae3391157085255d4c37dff"}, 304 | {file = "lesscpy-0.15.1.tar.gz", hash = "sha256:1045d17a98f688646ca758dff254e6e9c03745648e051a081b0395c3b77c824c"}, 305 | ] 306 | 307 | [package.dependencies] 308 | ply = "*" 309 | 310 | [[package]] 311 | name = "loguru" 312 | version = "0.7.3" 313 | description = "Python logging made (stupidly) simple" 314 | optional = false 315 | python-versions = "<4.0,>=3.5" 316 | groups = ["main"] 317 | files = [ 318 | {file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"}, 319 | {file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}, 320 | ] 321 | 322 | [package.dependencies] 323 | colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} 324 | win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} 325 | 326 | [package.extras] 327 | dev = ["Sphinx (==8.1.3)", "build (==1.2.2)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.5.0)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.13.0)", "mypy (==v1.4.1)", "myst-parser (==4.0.0)", "pre-commit (==4.0.1)", "pytest (==6.1.2)", "pytest (==8.3.2)", "pytest-cov (==2.12.1)", "pytest-cov (==5.0.0)", "pytest-cov (==6.0.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.1.0)", "sphinx-rtd-theme (==3.0.2)", "tox (==3.27.1)", "tox (==4.23.2)", "twine (==6.0.1)"] 328 | 329 | [[package]] 330 | name = "markupsafe" 331 | version = "3.0.2" 332 | description = "Safely add untrusted strings to HTML/XML markup." 333 | optional = false 334 | python-versions = ">=3.9" 335 | groups = ["main"] 336 | files = [ 337 | {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, 338 | {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, 339 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, 340 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, 341 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, 342 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, 343 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, 344 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, 345 | {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, 346 | {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, 347 | {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, 348 | {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, 349 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, 350 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, 351 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, 352 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, 353 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, 354 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, 355 | {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, 356 | {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, 357 | {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, 358 | {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, 359 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, 360 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, 361 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, 362 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, 363 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, 364 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, 365 | {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, 366 | {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, 367 | {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, 368 | {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, 369 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, 370 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, 371 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, 372 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, 373 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, 374 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, 375 | {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, 376 | {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, 377 | {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, 378 | {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, 379 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, 380 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, 381 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, 382 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, 383 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, 384 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, 385 | {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, 386 | {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, 387 | {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, 388 | {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, 389 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, 390 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, 391 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, 392 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, 393 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, 394 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, 395 | {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, 396 | {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, 397 | {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, 398 | ] 399 | 400 | [[package]] 401 | name = "mccabe" 402 | version = "0.7.0" 403 | description = "McCabe checker, plugin for flake8" 404 | optional = false 405 | python-versions = ">=3.6" 406 | groups = ["main"] 407 | files = [ 408 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 409 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 410 | ] 411 | 412 | [[package]] 413 | name = "mypy-extensions" 414 | version = "1.0.0" 415 | description = "Type system extensions for programs checked with the mypy type checker." 416 | optional = false 417 | python-versions = ">=3.5" 418 | groups = ["main"] 419 | files = [ 420 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 421 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 422 | ] 423 | 424 | [[package]] 425 | name = "packaging" 426 | version = "24.2" 427 | description = "Core utilities for Python packages" 428 | optional = false 429 | python-versions = ">=3.8" 430 | groups = ["main"] 431 | files = [ 432 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, 433 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, 434 | ] 435 | 436 | [[package]] 437 | name = "pathspec" 438 | version = "0.12.1" 439 | description = "Utility library for gitignore style pattern matching of file paths." 440 | optional = false 441 | python-versions = ">=3.8" 442 | groups = ["main"] 443 | files = [ 444 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 445 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 446 | ] 447 | 448 | [[package]] 449 | name = "platformdirs" 450 | version = "4.3.6" 451 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 452 | optional = false 453 | python-versions = ">=3.8" 454 | groups = ["main"] 455 | files = [ 456 | {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, 457 | {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, 458 | ] 459 | 460 | [package.extras] 461 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] 462 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] 463 | type = ["mypy (>=1.11.2)"] 464 | 465 | [[package]] 466 | name = "pluggy" 467 | version = "1.5.0" 468 | description = "plugin and hook calling mechanisms for python" 469 | optional = false 470 | python-versions = ">=3.8" 471 | groups = ["main"] 472 | files = [ 473 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 474 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 475 | ] 476 | 477 | [package.extras] 478 | dev = ["pre-commit", "tox"] 479 | testing = ["pytest", "pytest-benchmark"] 480 | 481 | [[package]] 482 | name = "ply" 483 | version = "3.11" 484 | description = "Python Lex & Yacc" 485 | optional = false 486 | python-versions = "*" 487 | groups = ["main"] 488 | files = [ 489 | {file = "ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"}, 490 | {file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"}, 491 | ] 492 | 493 | [[package]] 494 | name = "pycodestyle" 495 | version = "2.12.1" 496 | description = "Python style guide checker" 497 | optional = false 498 | python-versions = ">=3.8" 499 | groups = ["main"] 500 | files = [ 501 | {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, 502 | {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, 503 | ] 504 | 505 | [[package]] 506 | name = "pyflakes" 507 | version = "3.2.0" 508 | description = "passive checker of Python programs" 509 | optional = false 510 | python-versions = ">=3.8" 511 | groups = ["main"] 512 | files = [ 513 | {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, 514 | {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, 515 | ] 516 | 517 | [[package]] 518 | name = "pytest" 519 | version = "8.3.5" 520 | description = "pytest: simple powerful testing with Python" 521 | optional = false 522 | python-versions = ">=3.8" 523 | groups = ["main"] 524 | files = [ 525 | {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, 526 | {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, 527 | ] 528 | 529 | [package.dependencies] 530 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 531 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 532 | iniconfig = "*" 533 | packaging = "*" 534 | pluggy = ">=1.5,<2" 535 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 536 | 537 | [package.extras] 538 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 539 | 540 | [[package]] 541 | name = "python-dotenv" 542 | version = "1.0.1" 543 | description = "Read key-value pairs from a .env file and set them as environment variables" 544 | optional = false 545 | python-versions = ">=3.8" 546 | groups = ["main"] 547 | files = [ 548 | {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, 549 | {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, 550 | ] 551 | 552 | [package.extras] 553 | cli = ["click (>=5.0)"] 554 | 555 | [[package]] 556 | name = "simple-websocket" 557 | version = "1.1.0" 558 | description = "Simple WebSocket server and client for Python" 559 | optional = false 560 | python-versions = ">=3.6" 561 | groups = ["main"] 562 | files = [ 563 | {file = "simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c"}, 564 | {file = "simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4"}, 565 | ] 566 | 567 | [package.dependencies] 568 | wsproto = "*" 569 | 570 | [package.extras] 571 | dev = ["flake8", "pytest", "pytest-cov", "tox"] 572 | docs = ["sphinx"] 573 | 574 | [[package]] 575 | name = "tomli" 576 | version = "2.2.1" 577 | description = "A lil' TOML parser" 578 | optional = false 579 | python-versions = ">=3.8" 580 | groups = ["main"] 581 | markers = "python_version < \"3.11\"" 582 | files = [ 583 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, 584 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, 585 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, 586 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, 587 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, 588 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, 589 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, 590 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, 591 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, 592 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, 593 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, 594 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, 595 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, 596 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, 597 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, 598 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, 599 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, 600 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, 601 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, 602 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, 603 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, 604 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, 605 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, 606 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, 607 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, 608 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, 609 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, 610 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, 611 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, 612 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, 613 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, 614 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, 615 | ] 616 | 617 | [[package]] 618 | name = "typing-extensions" 619 | version = "4.12.2" 620 | description = "Backported and Experimental Type Hints for Python 3.8+" 621 | optional = false 622 | python-versions = ">=3.8" 623 | groups = ["main"] 624 | markers = "python_version < \"3.11\"" 625 | files = [ 626 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 627 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 628 | ] 629 | 630 | [[package]] 631 | name = "webassets" 632 | version = "2.0" 633 | description = "Media asset management for Python, with glue code for various web frameworks" 634 | optional = false 635 | python-versions = "*" 636 | groups = ["main"] 637 | files = [ 638 | {file = "webassets-2.0-py3-none-any.whl", hash = "sha256:a31a55147752ba1b3dc07dee0ad8c8efff274464e08bbdb88c1fd59ffd552724"}, 639 | {file = "webassets-2.0.tar.gz", hash = "sha256:167132337677c8cedc9705090f6d48da3fb262c8e0b2773b29f3352f050181cd"}, 640 | ] 641 | 642 | [[package]] 643 | name = "werkzeug" 644 | version = "3.1.3" 645 | description = "The comprehensive WSGI web application library." 646 | optional = false 647 | python-versions = ">=3.9" 648 | groups = ["main"] 649 | files = [ 650 | {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, 651 | {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, 652 | ] 653 | 654 | [package.dependencies] 655 | MarkupSafe = ">=2.1.1" 656 | 657 | [package.extras] 658 | watchdog = ["watchdog (>=2.3)"] 659 | 660 | [[package]] 661 | name = "win32-setctime" 662 | version = "1.2.0" 663 | description = "A small Python utility to set file creation time on Windows" 664 | optional = false 665 | python-versions = ">=3.5" 666 | groups = ["main"] 667 | markers = "sys_platform == \"win32\"" 668 | files = [ 669 | {file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"}, 670 | {file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"}, 671 | ] 672 | 673 | [package.extras] 674 | dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] 675 | 676 | [[package]] 677 | name = "wsproto" 678 | version = "1.2.0" 679 | description = "WebSockets state-machine based protocol implementation" 680 | optional = false 681 | python-versions = ">=3.7.0" 682 | groups = ["main"] 683 | files = [ 684 | {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, 685 | {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, 686 | ] 687 | 688 | [package.dependencies] 689 | h11 = ">=0.9.0,<1" 690 | 691 | [metadata] 692 | lock-version = "2.1" 693 | python-versions = ">=3.10,<4.0" 694 | content-hash = "5f8e46885916d3802de1fdad479b2b1edae97c1155b17bdd2c8587c2cd295f3b" 695 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "flask_assets_tutorial" 3 | version = "0.1.1" 4 | description = "Flask application which compiles ands serves frontend assets with Flask-Assets." 5 | authors = ["Todd Birchard "] 6 | maintainers = ["Todd Birchard "] 7 | license = "MIT" 8 | readme = "README.md" 9 | homepage = "https://hackersandslackers.com/flask-assets/" 10 | repository = "https://github.com/hackersandslackers/flask-assets-tutorial/" 11 | documentation = "https://hackersandslackers.com/flask-assets/" 12 | keywords = [ 13 | "Flask", 14 | "Flask-Assets", 15 | "Bundles", 16 | "Frontend", 17 | "Tutorial" 18 | ] 19 | 20 | [tool.poetry.dependencies] 21 | python = ">=3.12,<4.0" 22 | flask = "*" 23 | flask-assets = "*" 24 | lesscpy = "*" 25 | cssmin = "*" 26 | jsmin = "*" 27 | python-dotenv = "*" 28 | pytest = "*" 29 | black = "*" 30 | isort = "*" 31 | gunicorn = "*" 32 | flask-sock = "*" 33 | flake8 = "7.1.1" 34 | loguru = "^0.7.2" 35 | 36 | [tool.poetry.scripts] 37 | run = "wsgi:app" 38 | 39 | [tool.poetry.urls] 40 | issues = "https://github.com/hackersandslackers/flask-assets-tutorial/issues" 41 | 42 | [build-system] 43 | requires = ["poetry-core>=2.0.1,<3.0.0"] 44 | build-backend = "poetry.core.masonry.api" 45 | 46 | [tool.poetry.plugins] 47 | 48 | [tool.poetry.plugins."poetry.application.plugin"] 49 | export = "poetry_plugin_export.plugins:ExportApplicationPlugin" 50 | 51 | [tool.isort] 52 | profile = "black" 53 | src_paths = [ 54 | "flask_assets_tutorial", 55 | "config", 56 | "log" 57 | ] 58 | 59 | [tool.pylint.'MESSAGES CONTROL'] 60 | disable = "C0103,C0301,W0703,W0621" 61 | 62 | [tool.black] 63 | line-length = 120 64 | target-version = ['py312'] 65 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | black==25.1.0 ; python_version >= "3.10" and python_version < "4.0" 2 | blinker==1.9.0 ; python_version >= "3.10" and python_version < "4.0" 3 | click==8.1.8 ; python_version >= "3.10" and python_version < "4.0" 4 | colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" and platform_system == "Windows" or python_version >= "3.10" and python_version < "4.0" and sys_platform == "win32" 5 | cssmin==0.2.0 ; python_version >= "3.10" and python_version < "4.0" 6 | exceptiongroup==1.2.2 ; python_version >= "3.10" and python_version < "3.11" 7 | flake8==7.1.1 ; python_version >= "3.10" and python_version < "4.0" 8 | flask-assets==2.1.0 ; python_version >= "3.10" and python_version < "4.0" 9 | flask-sock==0.7.0 ; python_version >= "3.10" and python_version < "4.0" 10 | flask==3.1.0 ; python_version >= "3.10" and python_version < "4.0" 11 | gunicorn==23.0.0 ; python_version >= "3.10" and python_version < "4.0" 12 | h11==0.14.0 ; python_version >= "3.10" and python_version < "4.0" 13 | iniconfig==2.0.0 ; python_version >= "3.10" and python_version < "4.0" 14 | isort==6.0.1 ; python_version >= "3.10" and python_version < "4.0" 15 | itsdangerous==2.2.0 ; python_version >= "3.10" and python_version < "4.0" 16 | jinja2==3.1.6 ; python_version >= "3.10" and python_version < "4.0" 17 | jsmin==3.0.1 ; python_version >= "3.10" and python_version < "4.0" 18 | lesscpy==0.15.1 ; python_version >= "3.10" and python_version < "4.0" 19 | loguru==0.7.3 ; python_version >= "3.10" and python_version < "4.0" 20 | markupsafe==3.0.2 ; python_version >= "3.10" and python_version < "4.0" 21 | mccabe==0.7.0 ; python_version >= "3.10" and python_version < "4.0" 22 | mypy-extensions==1.0.0 ; python_version >= "3.10" and python_version < "4.0" 23 | packaging==24.2 ; python_version >= "3.10" and python_version < "4.0" 24 | pathspec==0.12.1 ; python_version >= "3.10" and python_version < "4.0" 25 | platformdirs==4.3.6 ; python_version >= "3.10" and python_version < "4.0" 26 | pluggy==1.5.0 ; python_version >= "3.10" and python_version < "4.0" 27 | ply==3.11 ; python_version >= "3.10" and python_version < "4.0" 28 | pycodestyle==2.12.1 ; python_version >= "3.10" and python_version < "4.0" 29 | pyflakes==3.2.0 ; python_version >= "3.10" and python_version < "4.0" 30 | pytest==8.3.5 ; python_version >= "3.10" and python_version < "4.0" 31 | python-dotenv==1.0.1 ; python_version >= "3.10" and python_version < "4.0" 32 | simple-websocket==1.1.0 ; python_version >= "3.10" and python_version < "4.0" 33 | tomli==2.2.1 ; python_version >= "3.10" and python_version < "3.11" 34 | typing-extensions==4.12.2 ; python_version >= "3.10" and python_version < "3.11" 35 | webassets==2.0 ; python_version >= "3.10" and python_version < "4.0" 36 | werkzeug==3.1.3 ; python_version >= "3.10" and python_version < "4.0" 37 | win32-setctime==1.2.0 ; python_version >= "3.10" and python_version < "4.0" and sys_platform == "win32" 38 | wsproto==1.2.0 ; python_version >= "3.10" and python_version < "4.0" 39 | -------------------------------------------------------------------------------- /wsgi.py: -------------------------------------------------------------------------------- 1 | """Application entry point.""" 2 | 3 | from flask_assets_tutorial import create_app 4 | 5 | app = create_app() 6 | --------------------------------------------------------------------------------