├── .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 | 
4 | 
5 | 
6 | 
7 | 
8 | [](https://github.com/hackersandslackers/flask-assets-tutorial/issues)
9 | [](https://github.com/hackersandslackers/flask-assets-tutorial/stargazers)
10 | [](https://github.com/hackersandslackers/flask-assets-tutorial/network)
11 |
12 | 
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 |
11 | - Blueprint: {{ request.blueprint }}
12 | - Template: {{ self._TemplateReference__context.name }}
13 | - View: {{ request.endpoint }}
14 | - Route: {{ request.path }}
15 |
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 |
--------------------------------------------------------------------------------