From b3667468709f10ca9ad3064c009018ed8a59641d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bompard?= Date: Fri, 25 Oct 2024 15:01:39 +0200 Subject: [PATCH] Add Flask webapp support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Aurélien Bompard --- README.md | 1 + cookiecutter.json | 4 +- hooks/post_gen_project.sh | 16 ++++ tests/test_basic.py | 8 +- .../deploy/{{ cookiecutter.slug }}.wsgi | 8 ++ {{ cookiecutter.slug }}/pyproject.toml | 18 +++- {{ cookiecutter.slug }}/tests/app_config.py | 7 ++ {{ cookiecutter.slug }}/tests/conftest.py | 31 ++++++- {{ cookiecutter.slug }}/tests/test_healthz.py | 61 +++++++++++++ {{ cookiecutter.slug }}/tests/test_root.py | 35 ++++++++ .../{{ cookiecutter.pkg_name }}/app.py | 90 +++++++++++++++++++ .../{{ cookiecutter.pkg_name }}/database.py | 3 + .../{{ cookiecutter.pkg_name }}/defaults.py | 17 ++++ .../{{ cookiecutter.pkg_name }}/l10n.py | 34 +++++++ .../templates/index.html | 27 ++++++ .../templates/main.html | 30 +++++++ .../templates/profile.html | 25 ++++++ .../utils/__init__.py | 10 +++ .../utils/healthz.py | 23 +++++ .../views/__init__.py | 8 ++ .../{{ cookiecutter.pkg_name }}/views/root.py | 22 +++++ .../{{ cookiecutter.slug }}.cfg.default | 12 +++ 22 files changed, 484 insertions(+), 6 deletions(-) create mode 100644 {{ cookiecutter.slug }}/deploy/{{ cookiecutter.slug }}.wsgi create mode 100644 {{ cookiecutter.slug }}/tests/app_config.py create mode 100644 {{ cookiecutter.slug }}/tests/test_healthz.py create mode 100644 {{ cookiecutter.slug }}/tests/test_root.py create mode 100644 {{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/app.py create mode 100644 {{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/defaults.py create mode 100644 {{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/l10n.py create mode 100644 {{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/templates/index.html create mode 100644 {{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/templates/main.html create mode 100644 {{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/templates/profile.html create mode 100644 {{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/utils/__init__.py create mode 100644 {{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/utils/healthz.py create mode 100644 {{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/views/__init__.py create mode 100644 {{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/views/root.py create mode 100644 {{ cookiecutter.slug }}/{{ cookiecutter.slug }}.cfg.default diff --git a/README.md b/README.md index ae2f49e..6959790 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Here are the libraries and services that are integrated: - Optionaly, CLI with [Click](click.palletsprojects.com/) - Optionaly, database support with [SQLAlchemy](https://www.sqlalchemy.org/) & [Alembic](https://alembic.sqlalchemy.org) +- Optionaly, Flask security with [Flask-Talisman](https://pypi.org/project/flask-talisman/) ## Requirements diff --git a/cookiecutter.json b/cookiecutter.json index 1ed249b..f5ec918 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -10,10 +10,12 @@ "with_sqlalchemy": false, "with_cli": false, "with_i18n": false, + "with_flask": false, "__prompts__": { "with_sqlalchemy": "Add database support with SQLAlchemy?", "with_cli": "Add a command line interface?", - "with_i18n": "Add support for translations (i18n)?" + "with_i18n": "Add support for translations (i18n)?", + "with_flask": "Is it a Flask application?" }, "_extensions": ["cookiecutter.extensions.RandomStringExtension"], "_copy_without_render": [ diff --git a/hooks/post_gen_project.sh b/hooks/post_gen_project.sh index d25789d..bb004a9 100644 --- a/hooks/post_gen_project.sh +++ b/hooks/post_gen_project.sh @@ -14,8 +14,24 @@ rm -rf "{{ cookiecutter.pkg_name }}/cli.py" {% if not cookiecutter.with_i18n %} rm -f "babel.cfg" rm -rf "{{ cookiecutter.pkg_name }}/translations" +rm -f "{{ cookiecutter.pkg_name }}/l10n.py" +{% endif %} +{% if not cookiecutter.with_flask %} +rm -rf "deploy" +rm -f "{{ cookiecutter.slug }}.cfg.default" +rm -f "{{ cookiecutter.pkg_name }}/app.py" +rm -f "{{ cookiecutter.pkg_name }}/defaults.py" +rm -f "{{ cookiecutter.pkg_name }}/l10n.py" +rm -rf "{{ cookiecutter.pkg_name }}/templates/" +rm -rf "{{ cookiecutter.pkg_name }}/utils/" +rm -rf "{{ cookiecutter.pkg_name }}/views/" +rm -f "tests/app_config.py" +rm -f "tests/test_healthz.py" +rm -f "tests/test_root.py" {% endif %} +black . +ruff check --fix . git init . git commit --allow-empty -s -m "Initial empty commit" git add . diff --git a/tests/test_basic.py b/tests/test_basic.py index 5f7a3ea..5c34b5b 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -45,12 +45,18 @@ def run(self, *args, **kwargs): @pytest.mark.parametrize("with_sqlalchemy", [False, True]) @pytest.mark.parametrize("with_cli", [False, True]) @pytest.mark.parametrize("with_i18n", [False, True]) -def test_basic_creation(tmpdir, with_sqlalchemy, with_cli, with_i18n): +@pytest.mark.parametrize("with_flask", [False, True]) +def test_basic_creation(tmpdir, with_sqlalchemy, with_cli, with_i18n, with_flask): + print("*"*40, with_sqlalchemy, with_cli, with_i18n, with_flask) + if with_flask and not with_sqlalchemy: + # Not supported + pytest.skip("Unsupported: flask without sqlalchemy") templated = Templated.create( tmpdir=tmpdir, with_sqlalchemy=with_sqlalchemy, with_cli=with_cli, with_i18n=with_i18n, + with_flask=with_flask, ) # We can't have 100% coverage with a templated project templated.run(["sed", "-i", "-e", "/^fail_under = 100/d", "pyproject.toml"]) diff --git a/{{ cookiecutter.slug }}/deploy/{{ cookiecutter.slug }}.wsgi b/{{ cookiecutter.slug }}/deploy/{{ cookiecutter.slug }}.wsgi new file mode 100644 index 0000000..44e6b57 --- /dev/null +++ b/{{ cookiecutter.slug }}/deploy/{{ cookiecutter.slug }}.wsgi @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: Contributors to the Fedora Project +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from {{ cookiecutter.pkg_name }}.app import create_app + + +application = create_app() diff --git a/{{ cookiecutter.slug }}/pyproject.toml b/{{ cookiecutter.slug }}/pyproject.toml index b477003..139c942 100644 --- a/{{ cookiecutter.slug }}/pyproject.toml +++ b/{{ cookiecutter.slug }}/pyproject.toml @@ -33,6 +33,10 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", +{% if cookiecutter.with_flask -%} + "Environment :: Web Environment", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", +{% endif %} ] [tool.poetry.dependencies] @@ -47,6 +51,17 @@ click = "^8.1.3" {% if cookiecutter.with_i18n -%} Babel = "^2.10.0" {%- endif %} +{% if cookiecutter.with_flask -%} +flask = "^3.0.0" +flask-wtf = ">=0.14" +flask-healthz = "^1.0.0" +flask-talisman = ">=0.8.0" +whitenoise = ">=5.3" +gunicorn = ">=20.0.0" +{% if cookiecutter.with_i18n -%} +flask-babel = "^4.0.0" +{%- endif %} +{%- endif %} [tool.poetry.group.dev.dependencies] pytest = ">=7.0.0" @@ -56,6 +71,7 @@ ruff = ">=0.1.1" coverage = {extras = ["toml"], version = ">=7.0.0"} diff-cover = ">=8.0.0" liccheck = ">=0.6" +towncrier = ">=21.3.0" pre-commit = ">=2.13" [tool.poetry.group.docs.dependencies] @@ -120,7 +136,7 @@ omit = [ [tool.towncrier] -package = "{{ cookiecutter.slug }}" +package = "{{ cookiecutter.pkg_name }}" filename = "docs/release_notes.md" directory = "changelog.d" start_string = "\n" diff --git a/{{ cookiecutter.slug }}/tests/app_config.py b/{{ cookiecutter.slug }}/tests/app_config.py new file mode 100644 index 0000000..711214e --- /dev/null +++ b/{{ cookiecutter.slug }}/tests/app_config.py @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: Contributors to the Fedora Project +# +# SPDX-License-Identifier: GPL-3.0-or-later + +TESTING = True +DEBUG = True +WTF_CSRF_ENABLED = False diff --git a/{{ cookiecutter.slug }}/tests/conftest.py b/{{ cookiecutter.slug }}/tests/conftest.py index 8a97724..9649105 100644 --- a/{{ cookiecutter.slug }}/tests/conftest.py +++ b/{{ cookiecutter.slug }}/tests/conftest.py @@ -2,8 +2,33 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -import pytest # noqa: F401 -{%- if cookiecutter.with_sqlalchemy %} +{% if cookiecutter.with_flask -%} +import os +{%- endif %} +import pytest {%- if not cookiecutter.with_flask %} # noqa: F401 {%- endif %} + +{% if cookiecutter.with_sqlalchemy -%} +from {{ cookiecutter.pkg_name }}.database import db {%- if not cookiecutter.with_flask %} # noqa: F401 {%- endif %} +{%- endif %} +{% if cookiecutter.with_flask -%} +from {{ cookiecutter.pkg_name }}.app import create_app + + +@pytest.fixture +def app(tmpdir): + app = create_app() + app.config.from_object("tests.app_config") + # Setup the DB + db_path = os.path.join(tmpdir, "database.sqlite") + app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{db_path}" + with app.app_context(): + db.manager.create() + return app + -from {{ cookiecutter.pkg_name }}.database import db # noqa: F401 +@pytest.fixture +def client(app): + with app.test_client() as client: + with app.app_context(): + yield client {%- endif %} diff --git a/{{ cookiecutter.slug }}/tests/test_healthz.py b/{{ cookiecutter.slug }}/tests/test_healthz.py new file mode 100644 index 0000000..2348d17 --- /dev/null +++ b/{{ cookiecutter.slug }}/tests/test_healthz.py @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: Contributors to the Fedora Project +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from flask import current_app +from sqlalchemy_helpers.manager import DatabaseStatus + +from {{ cookiecutter.pkg_name }}.database import db + + +def test_healthz_liveness(client): + """Test the /healthz/live check endpoint""" + response = client.get("/healthz/live") + assert response.status_code == 200 + assert response.json == {"status": 200, "title": "OK"} + + +def test_healthz_readiness_ok(client): + """Test the /healthz/ready check endpoint""" + response = client.get("/healthz/ready") + print(response.data) + assert response.status_code == 200 + assert response.json == {"status": 200, "title": "OK"} + + +def test_healthz_readiness_unavailable(client, mocker, tmpdir): + """Test the /healthz/ready check endpoint when the DB is not ready""" + db.manager.drop() + mocker.patch.dict( + current_app.config, {"SQLALCHEMY_DATABASE_URI": f"sqlite:///{tmpdir}/new.db"} + ) + response = client.get("/healthz/ready") + assert response.status_code == 503 + assert response.json == {"status": 503, "title": "Can't connect to the database"} + + +def test_healthz_readiness_needs_upgrade(client, mocker): + """Test the /healthz/ready check endpoint when the DB schema is old""" + mocker.patch.object( + db.manager, "get_status", return_value=DatabaseStatus.UPGRADE_AVAILABLE + ) + response = client.get("/healthz/ready") + assert response.status_code == 503 + assert response.json == { + "status": 503, + "title": "The database schema needs to be updated", + } + + +def test_healthz_readiness_exception(client, mocker): + """Test the /healthz/ready check endpoint when the DB is wrong""" + mocker.patch.dict( + current_app.config, {"SQLALCHEMY_DATABASE_URI": "sqlite:////does/not/exist"} + ) + response = client.get("/healthz/ready") + assert response.status_code == 503 + assert response.json["status"] == 503 + assert response.json["title"].startswith( + "Can't get the database status: (sqlite3.OperationalError) " + "unable to open database file" + ) diff --git a/{{ cookiecutter.slug }}/tests/test_root.py b/{{ cookiecutter.slug }}/tests/test_root.py new file mode 100644 index 0000000..efe0fcd --- /dev/null +++ b/{{ cookiecutter.slug }}/tests/test_root.py @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: Contributors to the Fedora Project +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from bs4 import BeautifulSoup + +from {{ cookiecutter.pkg_name }}.database import db +from {{ cookiecutter.pkg_name }}.models import User + + +def test_root(client): + """Test the root page""" + response = client.get("/") + assert response.status_code == 200 + page = BeautifulSoup(response.data, "html.parser") + assert page.title + assert page.title.string is not None + assert "{{ cookiecutter.name }}" in page.title.string + + +def test_profile(client): + """Test the profile page""" + user = User(name="testuser", full_name="Test User", timezone="Europe/Paris") + db.session.add(user) + db.session.flush() + response = client.get(f"/user/{user.id}") + assert response.status_code == 200 + page = BeautifulSoup(response.data, "html.parser") + assert page.title + assert page.title.string is not None + assert "{{ cookiecutter.name }}" in page.title.string + content = page.select_one("#profile") + assert content is not None + assert "Full Name: Test User" in list(content.stripped_strings) + assert "Timezone: Europe/Paris" in list(content.stripped_strings) diff --git a/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/app.py b/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/app.py new file mode 100644 index 0000000..567de9c --- /dev/null +++ b/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/app.py @@ -0,0 +1,90 @@ +# SPDX-FileCopyrightText: Contributors to the Fedora Project +# +# SPDX-License-Identifier: GPL-3.0-or-later + +import os +from logging.config import dictConfig + +import flask_talisman +from flask import Flask +from flask_healthz import healthz +from flask_wtf.csrf import CSRFProtect +from whitenoise import WhiteNoise + +{% if cookiecutter.with_i18n -%} +from {{ cookiecutter.pkg_name }} import l10n +{%- endif %} +from {{ cookiecutter.pkg_name }}.database import db +from {{ cookiecutter.pkg_name }}.utils import import_all +from {{ cookiecutter.pkg_name }}.views import blueprint + + +# Forms +csrf = CSRFProtect() + +# Security headers +talisman = flask_talisman.Talisman() + + +def create_app(config=None): + """See https://flask.palletsprojects.com/en/1.1.x/patterns/appfactories/""" + + app = Flask(__name__) + + # Load default configuration + app.config.from_object("{{ cookiecutter.pkg_name }}.defaults") + + # Load the optional configuration file + if "FLASK_CONFIG" in os.environ: + app.config.from_envvar("FLASK_CONFIG") + + # Load the config passed as argument + app.config.update(config or {}) + + if app.config.get("TEMPLATES_AUTO_RELOAD"): + app.jinja_env.auto_reload = True + + # Logging + if app.config.get("LOGGING"): + dictConfig(app.config["LOGGING"]) + + # Extensions +{%- if cookiecutter.with_i18n %} + l10n.babel.init_app(app, locale_selector=l10n.pick_locale, timezone_selector=l10n.get_timezone) + app.before_request(l10n.store_locale) + app.jinja_env.add_extension("jinja2.ext.i18n") +{%- endif %} + csrf.init_app(app) + + # Database + db.init_app(app) + + # Security + talisman.init_app( + app, + force_https=app.config.get("SESSION_COOKIE_SECURE", True), + session_cookie_secure=app.config.get("SESSION_COOKIE_SECURE", True), + frame_options=flask_talisman.DENY, + referrer_policy="same-origin", + content_security_policy={ + "default-src": ["'self'", "apps.fedoraproject.org"], + "script-src": [ + # https://csp.withgoogle.com/docs/strict-csp.html#example + "'strict-dynamic'", + ], + # "img-src": ["'self'", "seccdn.libravatar.org"], + }, + content_security_policy_nonce_in=["script-src"], + ) + + # Register views + import_all("{{ cookiecutter.pkg_name }}.views") + app.register_blueprint(blueprint) + app.register_blueprint(healthz, url_prefix="/healthz") + + # Static files + app.wsgi_app = WhiteNoise( + app.wsgi_app, root=f"{app.root_path}/static/", prefix="static/" + ) + + return app diff --git a/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/database.py b/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/database.py index f1608b3..859dbca 100644 --- a/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/database.py +++ b/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/database.py @@ -15,6 +15,9 @@ get_or_create, is_sqlite, ) +{% if cookiecutter.with_flask -%} +from sqlalchemy_helpers.flask_ext import get_or_404 # noqa: F401 +{%- endif %} from {{ cookiecutter.pkg_name }}.config import get_config diff --git a/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/defaults.py b/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/defaults.py new file mode 100644 index 0000000..60a6ad4 --- /dev/null +++ b/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/defaults.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: Contributors to the Fedora Project +# +# SPDX-License-Identifier: GPL-3.0-or-later + +# This file contains the default configuration values + +TEMPLATES_AUTO_RELOAD = False +SESSION_COOKIE_HTTPONLY = True +SESSION_COOKIE_SECURE = True + +SQLALCHEMY_TRACK_MODIFICATIONS = False +SQLALCHEMY_DATABASE_URI = "sqlite:///../{{ cookiecutter.slug }}.db" + +HEALTHZ = { + "live": "{{ cookiecutter.pkg_name }}.utils.healthz.liveness", + "ready": "{{ cookiecutter.pkg_name }}.utils.healthz.readiness", +} diff --git a/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/l10n.py b/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/l10n.py new file mode 100644 index 0000000..2b305b7 --- /dev/null +++ b/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/l10n.py @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: Contributors to the Fedora Project +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from flask import g, request +from flask_babel import Babel, get_locale + + +_LANGUAGES = [] + +babel = Babel() + + +def _get_accepted_languages(): + global _LANGUAGES + if not _LANGUAGES: + _LANGUAGES = [locale.language for locale in babel.list_translations()] + _LANGUAGES.sort() + return _LANGUAGES + + +def pick_locale(): + return request.accept_languages.best_match(_get_accepted_languages()) + + +def get_timezone(): + user = getattr(g, "user", None) + if user is not None: + return user.timezone + + +def store_locale(): + # Store the current locale in g for access in the templates. + g.locale = pick_locale() diff --git a/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/templates/index.html b/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/templates/index.html new file mode 100644 index 0000000..15ca339 --- /dev/null +++ b/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/templates/index.html @@ -0,0 +1,27 @@ +{% raw -%} +{# +SPDX-FileCopyrightText: Contributors to the Fedora Project + +SPDX-License-Identifier: GPL-3.0-or-later +#} + +{% extends "main.html" %} + +{% block content %} + {{ super() }} + +
+
+
+
+ {{ + {%- endraw -%} + _("Welcome to {{ cookiecutter.name }}") + {%- raw -%} + }} +
+
+
+
+{% endblock %} +{%- endraw %} diff --git a/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/templates/main.html b/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/templates/main.html new file mode 100644 index 0000000..95326dc --- /dev/null +++ b/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/templates/main.html @@ -0,0 +1,30 @@ +{% raw %} +{# +SPDX-FileCopyrightText: Contributors to the Fedora Project + +SPDX-License-Identifier: GPL-3.0-or-later +#} + + + + + + + {% block head %}{% endblock %} + + + + {%- block title -%} + {% endraw %}{{ cookiecutter.name }}{% raw %} + {%- endblock -%} + + + + {% block navbar %}{% endblock %} + {% block content %}{% endblock %} + {% block footer %}{% endblock %} + {% block scripts %} + {% endblock %} + + +{%- endraw %} diff --git a/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/templates/profile.html b/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/templates/profile.html new file mode 100644 index 0000000..6857a0c --- /dev/null +++ b/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/templates/profile.html @@ -0,0 +1,25 @@ +{% raw -%} +{# +SPDX-FileCopyrightText: Contributors to the Fedora Project + +SPDX-License-Identifier: GPL-3.0-or-later +#} + +{% extends "main.html" %} + +{% block title %} +Profile of {{ user.name }} | {{ super() }} +{% endblock %} + +{% block content %} + {{ super() }} + +
+
    +
  • {{ _("Username:") }} {{ user.name }}
  • +
  • {{ _("Full Name:") }} {{ user.full_name }}
  • +
  • {{ _("Timezone:") }} {{ user.timezone }}
  • +
+
+{% endblock %} +{%- endraw %} diff --git a/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/utils/__init__.py b/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/utils/__init__.py new file mode 100644 index 0000000..437e919 --- /dev/null +++ b/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/utils/__init__.py @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: Contributors to the Fedora Project +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from werkzeug.utils import find_modules, import_string + + +def import_all(import_name): + for module in find_modules(import_name, include_packages=True, recursive=True): + import_string(module) diff --git a/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/utils/healthz.py b/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/utils/healthz.py new file mode 100644 index 0000000..726c85e --- /dev/null +++ b/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/utils/healthz.py @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: Contributors to the Fedora Project +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from flask_healthz import HealthError +from sqlalchemy_helpers.manager import DatabaseStatus + +from {{ cookiecutter.pkg_name }}.database import db + + +def liveness(): + pass + + +def readiness(): + try: + status = db.manager.get_status() + except Exception as e: + raise HealthError(f"Can't get the database status: {e}") from e + if status is DatabaseStatus.NO_INFO: + raise HealthError("Can't connect to the database") + if status is DatabaseStatus.UPGRADE_AVAILABLE: + raise HealthError("The database schema needs to be updated") diff --git a/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/views/__init__.py b/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/views/__init__.py new file mode 100644 index 0000000..b857d3c --- /dev/null +++ b/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/views/__init__.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: Contributors to the Fedora Project +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from flask import Blueprint + + +blueprint = Blueprint("root", __name__) diff --git a/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/views/root.py b/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/views/root.py new file mode 100644 index 0000000..61a9bb8 --- /dev/null +++ b/{{ cookiecutter.slug }}/{{ cookiecutter.pkg_name }}/views/root.py @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: Contributors to the Fedora Project +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from flask import render_template + +from {{ cookiecutter.pkg_name }}.database import db, get_or_404 +from {{ cookiecutter.pkg_name }}.models import User + +from . import blueprint as bp + + +@bp.route("/") +def root(): + users = db.session.query(User).all() + return render_template("index.html", users=users) + + +@bp.route("/user/") +def profile(user_id): + user = get_or_404(User, user_id) + return render_template("profile.html", user=user) diff --git a/{{ cookiecutter.slug }}/{{ cookiecutter.slug }}.cfg.default b/{{ cookiecutter.slug }}/{{ cookiecutter.slug }}.cfg.default new file mode 100644 index 0000000..6c3faad --- /dev/null +++ b/{{ cookiecutter.slug }}/{{ cookiecutter.slug }}.cfg.default @@ -0,0 +1,12 @@ +TEMPLATES_AUTO_RELOAD = True + +# Database +SQLALCHEMY_DATABASE_URI = "sqlite:///../{{ cookiecutter.slug }}.db" + +# Session secret +SECRET_KEY = "{{ random_ascii_string(64, punctuation=True) }}" + +SESSION_COOKIE_HTTPONLY = True + +# Set to True for prod... +SESSION_COOKIE_SECURE = False