Add a config object

- add the following file:
  + src/urban_meal_delivery/_config.py
- a config module is created holding two sets of configurations:
  + production => against the real database
  + testing => against a database with test data
- the module is "protected" (i.e., underscore) and imported at the
  top level via a proxy-like object `config` that detects in which of
  the two environments the package is being run
This commit is contained in:
Alexander Hess 2020-08-08 14:43:02 +02:00
parent b42ceb4cea
commit 9456f86d65
Signed by: alexander
GPG key ID: 344EA5AB10D868E0
7 changed files with 170 additions and 3 deletions

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
.cache/
*.egg-info/
.env
.python-version
.venv/

17
poetry.lock generated
View file

@ -804,6 +804,17 @@ pytest = ">=4.6"
[package.extras]
testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"]
[[package]]
category = "main"
description = "Add .env support to your django/flask apps in development and deployments"
name = "python-dotenv"
optional = false
python-versions = "*"
version = "0.14.0"
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
category = "dev"
description = "World timezone definitions, modern and historical"
@ -1140,7 +1151,7 @@ optional = ["pygments", "colorama"]
tests = ["pytest", "pytest-cov", "codecov", "scikit-build", "cmake", "ninja", "pybind11"]
[metadata]
content-hash = "beb356287b912e1ce67c5997bd4d683dbc424ee24660a93d8f1fb4e511972762"
content-hash = "862a0bc650dab485af541f185ed3c1e86a8947e7518c8fbcacfd19d5b8055af5"
lock-version = "1.0"
python-versions = "^3.8"
@ -1526,6 +1537,10 @@ pytest-cov = [
{file = "pytest-cov-2.10.0.tar.gz", hash = "sha256:1a629dc9f48e53512fcbfda6b07de490c374b0c83c55ff7a1720b3fccff0ac87"},
{file = "pytest_cov-2.10.0-py2.py3-none-any.whl", hash = "sha256:6e6d18092dce6fad667cd7020deed816f858ad3b49d5b5e2b1cc1c97a4dba65c"},
]
python-dotenv = [
{file = "python-dotenv-0.14.0.tar.gz", hash = "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d"},
{file = "python_dotenv-0.14.0-py2.py3-none-any.whl", hash = "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423"},
]
pytz = [
{file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"},
{file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"},

View file

@ -28,6 +28,7 @@ repository = "https://github.com/webartifex/urban-meal-delivery"
python = "^3.8"
click = "^7.1.2"
python-dotenv = "^0.14.0"
[tool.poetry.dev-dependencies]
# Task Runners

View file

@ -109,6 +109,11 @@ per-file-ignores =
WPS213,
# No overuse of string constants (e.g., '--version').
WPS226,
src/urban_meal_delivery/_config.py:
# Allow upper case class variables within classes.
WPS115,
# Numbers are normal in config files.
WPS432,
tests/*.py:
# Type annotations are not strictly enforced.
ANN0, ANN2,
@ -135,10 +140,10 @@ show-source = true
# wemake-python-styleguide's settings
# ===================================
allowed-domain-names =
obj,
param,
result,
value,
min-name-length = 3
max-name-length = 40
# darglint
strictness = long
@ -186,7 +191,13 @@ single_line_exclusions = typing
[mypy]
cache_dir = .cache/mypy
[mypy-nox.*,packaging,pytest]
[mypy-dotenv]
ignore_missing_imports = true
[mypy-nox.*]
ignore_missing_imports = true
[mypy-packaging]
ignore_missing_imports = true
[mypy-pytest]
ignore_missing_imports = true

View file

@ -6,8 +6,11 @@ Example:
True
"""
import os as _os
from importlib import metadata as _metadata
from urban_meal_delivery import _config # noqa:WPS450
try:
_pkg_info = _metadata.metadata(__name__)
@ -21,3 +24,7 @@ else:
__author__ = _pkg_info['author']
__pkg_name__ = _pkg_info['name']
__version__ = _pkg_info['version']
# Little Hack: "Overwrites" the config module so that the environment is already set.
config = _config.get_config('testing' if _os.getenv('TESTING') else 'production')

View file

@ -0,0 +1,87 @@
"""Provide package-wide configuration.
This module is "protected" so that it is only used
via the `config` proxy at the package's top level.
That already loads the correct configuration
depending on the current environment.
"""
import datetime
import os
import warnings
import dotenv
dotenv.load_dotenv()
class Config:
"""Configuration that applies in all situations."""
# pylint:disable=too-few-public-methods
CUTOFF_DAY = datetime.datetime(2017, 2, 1)
# If a scheduled pre-order is made within this
# time horizon, we treat it as an ad-hoc order.
QUASI_AD_HOC_LIMIT = datetime.timedelta(minutes=45)
DATABASE_URI = os.getenv('DATABASE_URI')
# The PostgreSQL schema that holds the tables with the original data.
ORIGINAL_SCHEMA = os.getenv('ORIGINAL_SCHEMA') or 'public'
# The PostgreSQL schema that holds the tables with the cleaned data.
CLEAN_SCHEMA = os.getenv('CLEAN_SCHEMA') or 'clean'
def __repr__(self) -> str:
"""Non-literal text representation."""
return '<configuration>'
class ProductionConfig(Config):
"""Configuration for the real dataset."""
# pylint:disable=too-few-public-methods
TESTING = False
class TestingConfig(Config):
"""Configuration for the test suite."""
# pylint:disable=too-few-public-methods
TESTING = True
DATABASE_URI = os.getenv('DATABASE_URI_TESTING') or Config.DATABASE_URI
CLEAN_SCHEMA = os.getenv('CLEAN_SCHEMA_TESTING') or Config.CLEAN_SCHEMA
def get_config(env: str = 'production') -> Config:
"""Get the configuration for the package.
Args:
env: either 'production' or 'testing'; defaults to the first
Returns:
config: a namespace with all configurations
Raises:
ValueError: if `env` is not as specified
""" # noqa:DAR203
config: Config
if env.strip().lower() == 'production':
config = ProductionConfig()
elif env.strip().lower() == 'testing':
config = TestingConfig()
else:
raise ValueError("Must be either 'production' or 'testing'")
# Without a PostgreSQL database the package cannot work.
if config.DATABASE_URI is None:
warnings.warn('Bad configurartion: no DATABASE_URI set in the environment')
return config

45
tests/test_config.py Normal file
View file

@ -0,0 +1,45 @@
"""Test the package's configuration module."""
import pytest
from urban_meal_delivery import _config as config_mod # noqa:WPS450
envs = ['production', 'testing']
@pytest.mark.parametrize('env', envs)
def test_config_repr(env):
"""Config objects have the text representation '<configuration>'."""
config = config_mod.get_config(env)
assert str(config) == '<configuration>'
def test_invalid_config():
"""There are only 'production' and 'testing' configurations."""
with pytest.raises(ValueError, match="'production' or 'testing'"):
config_mod.get_config('invalid')
@pytest.mark.parametrize('env', envs)
def test_database_uri_set(env, monkeypatch):
"""Package does NOT emit warning if DATABASE_URI is set."""
uri = 'postgresql://user:password@localhost/db'
monkeypatch.setattr(config_mod.ProductionConfig, 'DATABASE_URI', uri)
monkeypatch.setattr(config_mod.TestingConfig, 'DATABASE_URI', uri)
with pytest.warns(None) as record:
config_mod.get_config(env)
assert len(record) == 0 # noqa:WPS441,WPS507
@pytest.mark.parametrize('env', envs)
def test_no_database_uri_set(env, monkeypatch):
"""Package does not work without DATABASE_URI set in the environment."""
monkeypatch.setattr(config_mod.ProductionConfig, 'DATABASE_URI', None)
monkeypatch.setattr(config_mod.TestingConfig, 'DATABASE_URI', None)
with pytest.warns(UserWarning, match='no DATABASE_URI'):
config_mod.get_config(env)