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:
parent
b42ceb4cea
commit
9456f86d65
7 changed files with 170 additions and 3 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,4 +1,5 @@
|
|||
.cache/
|
||||
*.egg-info/
|
||||
.env
|
||||
.python-version
|
||||
.venv/
|
||||
|
|
17
poetry.lock
generated
17
poetry.lock
generated
|
@ -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"},
|
||||
|
|
|
@ -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
|
||||
|
|
15
setup.cfg
15
setup.cfg
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
87
src/urban_meal_delivery/_config.py
Normal file
87
src/urban_meal_delivery/_config.py
Normal 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
45
tests/test_config.py
Normal 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)
|
Loading…
Reference in a new issue