"""
app.config
==========
Most configuration is set via environment variables as per
https://12factor.net/config:
"""
# pylint: disable=too-many-public-methods,invalid-name
from __future__ import annotations
from pathlib import Path
import tomli
from environs import Env
from flask import Flask
[docs]
class Config(Env):
"""The application's configuration object.
List objects are comma separated values.
.. code-block:: console
LIST=comma,separated,values
Dict objects are comma separated variable assignments.
.. code-block:: console
DICT=comma=0,separated=1,values=2
:param root_path: The application's filesystem, starting at the
root.
"""
def __init__(self, root_path: Path) -> None:
super().__init__()
self._root_path = root_path
@property
def DEBUG(self) -> bool:
"""Run the application in debug mode.
Default value is ``False``.
"""
return self.bool("FLASK_DEBUG", default=False)
@property
def TESTING(self) -> bool:
"""Testing mode.
Default value is ``False``.
"""
return self.bool("TESTING", default=False)
@property
def DATABASE_URL(self) -> str:
"""Database URL.
Default value is ``None``.
"""
return self.str("DATABASE_URL", default="sqlite:///:memory:")
@property
def SECRET_KEY(self) -> str | None:
"""Key for security related needs.
Used for securely signing the session cookie and can be used for
any other security related needs by extensions or the
application. It should be a long random bytes or str.
For example:
.. code-block:: console
$ python -c 'import secrets; print(secrets.token_hex())'
'192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d547...'
Default value is ``None``.
"""
return self.str("SECRET_KEY", default=None)
@property
def SEND_FILE_MAX_AGE_DEFAULT(self) -> int:
"""Timedelta used as ``cache_timeout`` for ``send_file`` funcs.
This configuration variable can also be set with an integer
value used as seconds.
Default value is ``31556926``.
"""
return self.int("SEND_FILE_MAX_AGE_DEFAULT", default=31556926)
@property
def BCRYPT_LOG_ROUNDS(self) -> int:
"""Determine the complexity of bcrypt encryption.
see bcrypt for more details.
Default value is ``13``.
"""
return self.int("BCRYPT_LOG_ROUNDS", default=13)
@property
def DEBUG_TB_ENABLED(self) -> bool:
"""When ``DEBUG`` is True enable ``TB`` (toolbar)."""
return self.bool("DEBUG_TB_ENABLED", default=self.DEBUG)
@property
def DEBUG_TB_INTERCEPT_REDIRECTS(self) -> bool:
"""Debug-toolbar to intercept redirects.
Default value is ``False``.
"""
return self.bool("DEBUG_TB_INTERCEPT_REDIRECTS", default=False)
@property
def FLASK_STATIC_DIGEST_HOST_URL(self) -> str | None:
"""Set to a value such as https://cdn.example.com.
Prefix your static path with this URL. This would be useful if
you host your files from a CDN. Make sure to include the
protocol (aka. https://).
Default value is ``None``.
"""
return self.str("FLASK_STATIC_DIGEST_HOST_URL", default=None)
@property
def FLASK_STATIC_DIGEST_BLACKLIST_FILTER(self) -> list[str]:
"""Do not md5 tag added extensions.
eg: [".htm", ".html", ".txt"]. Make sure to include the ".".
Default value is ``[]``.
"""
return self.list("FLASK_STATIC_DIGEST_BLACKLIST_FILTER", default=[])
@property
def SQLALCHEMY_DATABASE_URI(self) -> str:
"""The database URI that should be used for the connection.
Default value is ``"sqlite:///:memory:"``.
"""
# this was the default in 2.x.x, now if this does not get set
# before a certain action a runtime error is raised
return self.str(
"SQLALCHEMY_DATABASE_URI",
default=self.DATABASE_URL.replace("postgres://", "postgresql://"),
)
@property
def SQLALCHEMY_TRACK_MODIFICATIONS(self) -> bool:
"""Track modifications of objects and emit signals.
This requires extra memory and should be disabled if not needed.
Default value is ``False``.
"""
return self.bool("SQlALCHEMY_TRACK_MODIFICATIONS", default=False)
@property
def DEFAULT_MAIL_SENDER(self) -> str | None:
"""Default mail sender.
Default value is ``None``.
"""
return self.str("DEFAULT_MAIL_SENDER", default=None)
@property
def MAIL_SUBJECT_PREFIX(self) -> str:
"""Prefix for mail subject.
Default value is ``""``.
"""
return self.str("MAIL_SUBJECT_PREFIX", default="")
@property
def ADMINS(self) -> list[str]:
"""List of web admins.
Default value is ``[]``.
"""
return self.list("ADMINS", default=[])
@property
def WTF_CSRF_ENABLED(self) -> bool:
"""Cross-Site Request Forgery: Switch off for testing.
Default value is ``TESTING``.
"""
return self.bool("WTF_CSRF_ENABLED", default=not self.TESTING)
@property
def WTF_CSRF_SECRET_KEY(self) -> str:
"""Secret key for signing CSRF tokens.
Default value is ``SECRET_KEY``.
"""
return self.str("WTF_CSRF_SECRET_KEY", self.SECRET_KEY)
@property
def SECURITY_PASSWORD_SALT(self) -> str | None:
"""Specifies the HMAC salt.
This is only used if the password hash type is set to something
other than plain text.
Default value is ``SECRET_KEY``.
"""
return self.str("SECURITY_PASSWORD_SALT", default=self.SECRET_KEY)
@property
def MAIL_SERVER(self) -> str:
"""Mail server to send mail from.
Default value is ``None``.
"""
return self.str("MAIL_SERVER", default=None)
@property
def MAIL_PORT(self) -> int:
"""Mail port to send mail from.
Default value is ``25``.
"""
return self.int("MAIL_PORT", default=25)
@property
def MAIL_USE_TLS(self) -> bool:
"""Use TLS for emails.
Default value is ``False``.
"""
return self.bool("MAIL_USER_TLS", default=False)
@property
def MAIL_USE_SSL(self) -> bool:
"""Use SSL for emails.
Default value is ``False``.
"""
return self.bool("MAIL_USER_SSL", default=False)
@property
def MAIL_USERNAME(self) -> str | None:
"""Username for mail server.
Default value is ``None``.
"""
return self.str("MAIL_USERNAME", default=None)
@property
def MAIL_PASSWORD(self) -> str | None:
"""Password for mail server.
Default value is ``None``.
"""
return self.str("MAIL_PASSWORD", default=None)
@property
def POSTS_PER_PAGE(self) -> int:
"""Posts to paginate per page.
Default value is ``25``.
"""
return self.int("POSTS_PER_PAGE", default=25)
@property
def RESERVED_USERNAMES(self) -> list[str]:
"""List of names that cannot be registered.
Default value is ``[]``.
"""
return self.list("RESERVED_USERNAMES", default=[])
@property
def BRAND(self) -> str:
"""App branding to display on navbar and browser tabs.
Default value is ``""``.
"""
return self.str("BRAND", default="")
@property
def COPYRIGHT(self) -> str:
"""Return the copyright line from LICENSE else None."""
return self.str(
"COPYRIGHT",
default=(self._root_path.parent / "LICENSE")
.read_text(encoding="utf-8")
.splitlines()[2],
)
@property
def COPYRIGHT_YEAR(self) -> str:
"""Year the copyright is valid for.
By default this will be parsed from the LICENSE of this
repository.
"""
return self.str("COPYRIGHT_YEAR", default=self.COPYRIGHT.split()[2])
@property
def COPYRIGHT_AUTHOR(self) -> str:
"""Copyright holder of the application.
By default this will be parsed from the LICENSE of this
repository.
"""
return self.str(
"COPYRIGHT_AUTHOR", default=" ".join(self.COPYRIGHT.split()[3:])
)
@property
def COPYRIGHT_EMAIL(self) -> str:
"""Copyright holder's email.
By default this will be parsed from the pyproject.toml of this
repository.
"""
return self.str(
"COPYRIGHT_EMAIL",
default=tomli.loads(
(self._root_path.parent / "pyproject.toml").read_text()
)
.get("tool", {})
.get("poetry", {})
.get("authors", [])[0]
.split()[1][1:-1],
)
@property
def NAVBAR_HOME(self) -> bool:
"""Include ``Home`` in navbar as opposed to only the brand.
Default value is ``True``.
"""
return self.bool("NAVBAR_HOME", default=True)
@property
def NAVBAR_ICONS(self) -> bool:
"""Display certain links in navbar as icons instead of text.
Default value is ``False``.
"""
return self.bool("NAVBAR_ICONS", default=False)
@property
def NAVBAR_USER_DROPDOWN(self) -> bool:
"""Display logged-in user links as dropdown.
Default value is ``False``.
"""
return self.bool("NAVBAR_USER_DROPDOWN", default=False)
@property
def SESSION_COOKIE_HTTPONLY(self) -> bool:
"""Don't allow JavaScript access to marked cookies.
Default value is ``True``.
"""
return self.bool("SESSION_COOKIE_HTTPONLY", default=True)
@property
def SESSION_COOKIE_SECURE(self) -> bool:
"""Only send cookies with requests over HTTPS.
If the cookie is marked "secure" the application must be served
over HTTPS for this to make sense.
Default value is ``True``.
"""
return self.bool("SESSION_COOKIE_SECURE", default=True)
@property
def SESSION_COOKIE_SAMESITE(self) -> str:
"""Restrict how cookies are sent with external requests.
Default value is ``"strict"``.
"""
return self.str("REMEMBER_COOKIE_SAMESITE", default="strict")
@property
def REMEMBER_COOKIE_SECURE(self) -> bool:
"""Set remember-cookie secure parameter.
The cookie for the ``Remember Me`` checkbox in logins.
Default value is ``True``.
"""
return self.bool("REMEMBER_COOKIE_SECURE", default=True)
@property
def PREFERRED_URL_SCHEME(self) -> str:
"""Whether HTTP or HTTPS is preferred.
Default value is ``https``.
"""
return self.str("PREFERRED_URL_SCHEME", default="https")
@property
def CSP(self) -> dict[str, str | list[str]]:
"""Policies to add to existing `Content Security Policy`_.
.. _Content Security Policy:
https://github.com/jshwi/jss/blob/master/app/schemas/
csp.json
Default value is ``{}``.
"""
return self.dict("CSP", default={})
@property
def CSP_REPORT_ONLY(self) -> bool:
"""Do not enforce CSP and only report violations.
Default value is ``True``.
"""
return self.bool("CSP_REPORT_ONLY", default=True)
@property
def BOOTSTRAP_SERVE_LOCAL(self) -> bool:
"""Bootstrap resources will be served from the local app.
See `CDN support`_ for details.
.. _CDN support:
https://pythonhosted.org/Flask-Bootstrap/cdn.html
Default value is ``True``.
"""
return self.bool("BOOTSTRAP_SERVE_LOCAL", default=True)
@property
def SIGNATURE(self) -> str:
"""Signature to sign emails off with.
Default value is ``BRAND``.
"""
return self.str("SIGNATURE", default=self.BRAND)
@property
def ADMIN_SECRET(self) -> str:
"""Password for initialized admin user.
Default value is ``None``.
"""
return self.str("ADMIN_SECRET", default=None)
@property
def TRANSLATIONS_DIR(self) -> Path:
"""Path to translation files.
Default value is ``app.root_path / "translations"``.
"""
return self.path(
"TRANSLATIONS_DIR", default=self._root_path / "translations"
)
@property
def LANGUAGES(self) -> list[str]:
"""Supported languages.
Default value is ``["en"]``.
"""
return self.list(
"LANGUAGES",
default=["es"] + [p.name for p in self.TRANSLATIONS_DIR.iterdir()],
)
@property
def SHOW_POSTS(self) -> bool:
"""Show posts from database.
Default value is ``True``.
"""
return self.bool("SHOW_POSTS", default=True)
@property
def TITLE(self) -> str:
"""Header for page.
Default value is ``""``.
"""
return self.str("TITLE", default="")
@property
def SHOW_REGISTER(self) -> bool:
"""Show ``Register`` option in navbar.
Default value is ``True``.
"""
return self.bool("SHOW_REGISTER", default=True)
@property
def SHOW_PAYMENT(self) -> bool:
"""Show payment option.
Default value is ``False``.
"""
return self.bool("SHOW_PAYMENT", default=False)
@property
def STRIPE_SECRET_KEY(self) -> str:
"""Secret key for Stripe API.
Default value is ``None``.
"""
return self.str("STRIPE_SECRET_KEY", default=None)
@property
def PAYMENT_OPTIONS(self) -> dict[str, str]:
"""Key-value pairs of payment options for Stripe's API.
Price is in US cents.
Default value is ``{"price": "None"}``.
"""
return self.dict("PAYMENT_OPTIONS", default={"price": "None"})
@property
def MAX_CONTENT_LENGTH(self) -> int:
"""Max content length for file uploads.
Default value is ``1048576``.
"""
return self.int("MAX_CONTENT_LENGTH", default=1024 * 1024)
@property
def UPLOAD_EXTENSIONS(self) -> list[str]:
"""Extensions allowed for upload.
Default value is
``[".txt", ".pdf", ".png", ".jpg", ".jpeg", ".gif"]``.
"""
return self.list(
"UPLOAD_EXTENSIONS",
default=[".txt", ".pdf", ".png", ".jpg", ".jpeg", ".gif"],
)
@property
def STATIC_FOLDER(self) -> Path:
"""Path to static files.
Default value is ``app.root_path / "static"``.
"""
return self.path("STATIC_FOLDER", default=self._root_path / "static")
@property
def UPLOAD_PATH(self) -> Path:
"""Path to upload file to.
Default value is ``"${STATIC_FOLDER}/uploads"``.
"""
return self.path("UPLOAD_PATH", default=self.STATIC_FOLDER / "uploads")
@property
def SITEMAP_INCLUDE_RULES_WITHOUT_PARAMS(self) -> bool:
"""Generate list of rules without params.
Default value is ``True``.
"""
return self.bool("SITEMAP_INCLUDE_RULES_WITHOUT_PARAMS", default=False)
@property
def SITEMAP_CHANGEFREQ(self) -> str:
"""Frequency of change."""
return self.str("SITEMAP_CHANGEFREQ", default="monthly")
@property
def SITEMAP_URL_SCHEME(self) -> str:
"""URL scheme for sitemap."""
return self.str("SITEMAP_URL_SCHEME", default="https")
@property
def SCHEMAS(self) -> Path:
"""Path to schemas.
Default value is ``app.root_path / "schemas"``.
"""
return self.path("SCHEMAS", default=self._root_path / "schemas")
[docs]
def init_app(app: Flask) -> None:
"""Initialize config for this application.
:param app: Application object.
"""
config = Config(Path(app.root_path))
config.read_env()
app.config.from_object(config)
app.static_folder = app.config["STATIC_FOLDER"]