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/
|
.cache/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
|
.env
|
||||||
.python-version
|
.python-version
|
||||||
.venv/
|
.venv/
|
||||||
|
|
17
poetry.lock
generated
17
poetry.lock
generated
|
@ -804,6 +804,17 @@ pytest = ">=4.6"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"]
|
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]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "World timezone definitions, modern and historical"
|
description = "World timezone definitions, modern and historical"
|
||||||
|
@ -1140,7 +1151,7 @@ optional = ["pygments", "colorama"]
|
||||||
tests = ["pytest", "pytest-cov", "codecov", "scikit-build", "cmake", "ninja", "pybind11"]
|
tests = ["pytest", "pytest-cov", "codecov", "scikit-build", "cmake", "ninja", "pybind11"]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
content-hash = "beb356287b912e1ce67c5997bd4d683dbc424ee24660a93d8f1fb4e511972762"
|
content-hash = "862a0bc650dab485af541f185ed3c1e86a8947e7518c8fbcacfd19d5b8055af5"
|
||||||
lock-version = "1.0"
|
lock-version = "1.0"
|
||||||
python-versions = "^3.8"
|
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.tar.gz", hash = "sha256:1a629dc9f48e53512fcbfda6b07de490c374b0c83c55ff7a1720b3fccff0ac87"},
|
||||||
{file = "pytest_cov-2.10.0-py2.py3-none-any.whl", hash = "sha256:6e6d18092dce6fad667cd7020deed816f858ad3b49d5b5e2b1cc1c97a4dba65c"},
|
{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 = [
|
pytz = [
|
||||||
{file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"},
|
{file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"},
|
||||||
{file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"},
|
{file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"},
|
||||||
|
|
|
@ -28,6 +28,7 @@ repository = "https://github.com/webartifex/urban-meal-delivery"
|
||||||
python = "^3.8"
|
python = "^3.8"
|
||||||
|
|
||||||
click = "^7.1.2"
|
click = "^7.1.2"
|
||||||
|
python-dotenv = "^0.14.0"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
# Task Runners
|
# Task Runners
|
||||||
|
|
15
setup.cfg
15
setup.cfg
|
@ -109,6 +109,11 @@ per-file-ignores =
|
||||||
WPS213,
|
WPS213,
|
||||||
# No overuse of string constants (e.g., '--version').
|
# No overuse of string constants (e.g., '--version').
|
||||||
WPS226,
|
WPS226,
|
||||||
|
src/urban_meal_delivery/_config.py:
|
||||||
|
# Allow upper case class variables within classes.
|
||||||
|
WPS115,
|
||||||
|
# Numbers are normal in config files.
|
||||||
|
WPS432,
|
||||||
tests/*.py:
|
tests/*.py:
|
||||||
# Type annotations are not strictly enforced.
|
# Type annotations are not strictly enforced.
|
||||||
ANN0, ANN2,
|
ANN0, ANN2,
|
||||||
|
@ -135,10 +140,10 @@ show-source = true
|
||||||
# wemake-python-styleguide's settings
|
# wemake-python-styleguide's settings
|
||||||
# ===================================
|
# ===================================
|
||||||
allowed-domain-names =
|
allowed-domain-names =
|
||||||
|
obj,
|
||||||
param,
|
param,
|
||||||
result,
|
result,
|
||||||
value,
|
value,
|
||||||
min-name-length = 3
|
|
||||||
max-name-length = 40
|
max-name-length = 40
|
||||||
# darglint
|
# darglint
|
||||||
strictness = long
|
strictness = long
|
||||||
|
@ -186,7 +191,13 @@ single_line_exclusions = typing
|
||||||
[mypy]
|
[mypy]
|
||||||
cache_dir = .cache/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
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,11 @@ Example:
|
||||||
True
|
True
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os as _os
|
||||||
from importlib import metadata as _metadata
|
from importlib import metadata as _metadata
|
||||||
|
|
||||||
|
from urban_meal_delivery import _config # noqa:WPS450
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_pkg_info = _metadata.metadata(__name__)
|
_pkg_info = _metadata.metadata(__name__)
|
||||||
|
@ -21,3 +24,7 @@ else:
|
||||||
__author__ = _pkg_info['author']
|
__author__ = _pkg_info['author']
|
||||||
__pkg_name__ = _pkg_info['name']
|
__pkg_name__ = _pkg_info['name']
|
||||||
__version__ = _pkg_info['version']
|
__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