Skip to content

Commit

Permalink
Merge pull request #2 from maykinmedia/feature/1-config-command
Browse files Browse the repository at this point in the history
setup_notification command
  • Loading branch information
annashamray authored Mar 21, 2024
2 parents 662a8b4 + c17c48a commit e4559f7
Show file tree
Hide file tree
Showing 15 changed files with 504 additions and 36 deletions.
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,24 @@ jobs:
strategy:
matrix:
python: ['3.10', '3.11', '3.12']
django: ['4.2']
django: ['3.2', '4.2']

name: Run the test suite (Python $, Django $)
name: Run the test suite (Python ${{ matrix.python }}, Django ${{ matrix.django }})

steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: $
python-version: ${{ matrix.python }}

- name: Install dependencies
run: pip install tox tox-gh-actions

- name: Run tests
run: tox
env:
PYTHON_VERSION: $
DJANGO: $
PYTHON_VERSION: ${{ matrix.python }}
DJANGO: ${{ matrix.django }}

- name: Publish coverage report
uses: codecov/codecov-action@v3
Expand Down Expand Up @@ -61,4 +61,4 @@ jobs:
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: $
password: ${{ secrets.PYPI_TOKEN }}
2 changes: 1 addition & 1 deletion .github/workflows/code_quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ jobs:
run: pip install tox
- run: tox
env:
TOXENV: $
TOXENV: ${{ matrix.toxenv }}
69 changes: 69 additions & 0 deletions django_setup_configuration/configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from abc import ABC, abstractmethod

from django.conf import settings

from .exceptions import PrerequisiteFailed


class BaseConfigurationStep(ABC):
verbose_name: str
required_settings: list[str] = []
enable_setting: str = ""

def __repr__(self):
return self.verbose_name

def validate_requirements(self) -> None:
"""
check prerequisites of the configuration
:raises: :class: `django_setup_configuration.exceptions.PrerequisiteFailed`
if prerequisites are missing
"""
missing = [
var
for var in self.required_settings
if getattr(settings, var, None) in [None, ""]
]
if missing:
raise PrerequisiteFailed(
f"{', '.join(missing)} settings should be provided"
)

def is_enabled(self) -> bool:
"""
Hook to switch on and off the configuration step from env vars
By default all steps are enabled
"""
if not self.enable_setting:
return True

return getattr(settings, self.enable_setting, True)

@abstractmethod
def is_configured(self) -> bool:
"""
Check that the configuration is already done with current env variables
"""
...

@abstractmethod
def configure(self) -> None:
"""
Run the configuration step.
:raises: :class: `django_setup_configuration.exceptions.ConfigurationRunFailed`
if the configuration has an error
"""
...

@abstractmethod
def test_configuration(self) -> None:
"""
Test that the configuration works as expected
:raises: :class:`openzaak.config.bootstrap.exceptions.SelfTestFailure`
if the configuration aspect was found to be faulty.
"""
...
22 changes: 22 additions & 0 deletions django_setup_configuration/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class ConfigurationException(Exception):
"""
Base exception for configuration steps
"""


class PrerequisiteFailed(ConfigurationException):
"""
Raises an error when the configuration step can't be started
"""


class ConfigurationRunFailed(ConfigurationException):
"""
Raises an error when the configuration process was faulty
"""


class SelfTestFailed(ConfigurationException):
"""
Raises an error for failed configuration self-tests.
"""
Empty file.
Empty file.
115 changes: 115 additions & 0 deletions django_setup_configuration/management/commands/setup_configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
from django.conf import settings
from django.core.management import BaseCommand, CommandError
from django.db import transaction
from django.utils.module_loading import import_string

from ...configuration import BaseConfigurationStep
from ...exceptions import ConfigurationRunFailed, PrerequisiteFailed, SelfTestFailed


class ErrorDict(dict):
"""
small helper to display errors
"""

def as_text(self) -> str:
output = [f"{k}: {v}" for k, v in self.items()]
return "\n".join(output)


class Command(BaseCommand):
help = (
"Bootstrap the initial configuration of the application. "
"This command is run only in non-interactive mode with settings "
"configured mainly via environment variables."
)
output_transaction = True

def add_arguments(self, parser):
parser.add_argument(
"--overwrite",
action="store_true",
help=(
"Overwrite the existing configuration. Should be used if some "
"of the env variables have been changed."
),
)
parser.add_argument(
"--no-selftest",
action="store_true",
dest="skip_selftest",
help=(
"Skip checking if configuration is successful. Use it if you "
"run this command in the init container before the web app is started"
),
)

@transaction.atomic
def handle(self, **options):
overwrite: bool = options["overwrite"]
skip_selftest: bool = options["skip_selftest"]

errors = ErrorDict()
steps: list[BaseConfigurationStep] = [
import_string(path)() for path in settings.SETUP_CONFIGURATION_STEPS
]
enabled_steps = [step for step in steps if step.is_enabled()]

if not enabled_steps:
self.stdout.write(
"There are no enabled configuration steps. "
"Configuration can't be set up"
)
return

self.stdout.write(
f"Configuration will be set up with following steps: {enabled_steps}"
)

# 1. Check prerequisites of all steps
for step in enabled_steps:
try:
step.validate_requirements()
except PrerequisiteFailed as exc:
errors[step] = exc

if errors:
raise CommandError(
f"Prerequisites for configuration are not fulfilled: {errors.as_text()}"
)

# 2. Configure steps
configured_steps = []
for step in enabled_steps:
if not overwrite and step.is_configured():
self.stdout.write(
f"Step {step} is skipped, because the configuration already exists."
)
continue
else:
self.stdout.write(f"Configuring {step}...")
try:
step.configure()
except ConfigurationRunFailed as exc:
raise CommandError(f"Could not configure step {step}") from exc
else:
self.stdout.write(f"{step} is successfully configured")
configured_steps.append(step)

# 3. Test configuration
if skip_selftest:
self.stdout.write("Selftest is skipped.")

else:
for step in configured_steps:
try:
step.test_configuration()
except SelfTestFailed as exc:
errors[step] = exc

if errors:
raise CommandError(
f"Configuration test failed with errors: {errors.as_text()}"
)

self.stdout.write(self.style.SUCCESS("Instance configuration completed."))
40 changes: 21 additions & 19 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,20 @@
contain the root `toctree` directive.
Welcome to django_setup_configuration's documentation!
=================================================
======================================================

|build-status| |code-quality| |black| |coverage| |docs|
|build-status| |code-quality| |black| |coverage|

|python-versions| |django-versions| |pypi-version|
..
|docs| |python-versions| |django-versions| |pypi-version|
<One liner describing the project>
Django Setup Configuration allows to make a pluggable configuration setup
used with the django management command.

Features
========

* ...
* ...
* management command, which runs the ordered list of all configuration steps

.. toctree::
:maxdepth: 2
Expand All @@ -33,28 +34,29 @@ Indices and tables
* :ref:`search`


.. |build-status| image:: https://github.com/maykinmedia/django_setup_configuration/workflows/Run%20CI/badge.svg
.. |build-status| image:: https://github.com/maykinmedia/django-setup-configuration/workflows/Run%20CI/badge.svg
:alt: Build status
:target: https://github.com/maykinmedia/django_setup_configuration/actions?query=workflow%3A%22Run+CI%22
:target: https://github.com/maykinmedia/django-setup-configuration/actions?query=workflow%3A%22Run+CI%22

.. |code-quality| image:: https://github.com/maykinmedia/django_setup_configuration/workflows/Code%20quality%20checks/badge.svg
.. |code-quality| image:: https://github.com/maykinmedia/django-setup-configuration/workflows/Code%20quality%20checks/badge.svg
:alt: Code quality checks
:target: https://github.com/maykinmedia/django_setup_configuration/actions?query=workflow%3A%22Code+quality+checks%22
:target: https://github.com/maykinmedia/django-setup-configuration/actions?query=workflow%3A%22Code+quality+checks%22

.. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/psf/black

.. |coverage| image:: https://codecov.io/gh/maykinmedia/django_setup_configuration/branch/master/graph/badge.svg
:target: https://codecov.io/gh/maykinmedia/django_setup_configuration
.. |coverage| image:: https://codecov.io/gh/maykinmedia/django-setup-configuration/branch/main/graph/badge.svg
:target: https://codecov.io/gh/maykinmedia/django-setup-configuration
:alt: Coverage status

.. |docs| image:: https://readthedocs.org/projects/django_setup_configuration/badge/?version=latest
:target: https://django_setup_configuration.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status
..
.. |docs| image:: https://readthedocs.org/projects/django_setup_configuration/badge/?version=latest
:target: https://django_setup_configuration.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status
.. |python-versions| image:: https://img.shields.io/pypi/pyversions/django_setup_configuration.svg
.. |python-versions| image:: https://img.shields.io/pypi/pyversions/django-setup-configuration.svg

.. |django-versions| image:: https://img.shields.io/pypi/djversions/django_setup_configuration.svg
.. |django-versions| image:: https://img.shields.io/pypi/djversions/django-setup-configuration.svg

.. |pypi-version| image:: https://img.shields.io/pypi/v/django_setup_configuration.svg
:target: https://pypi.org/project/django_setup_configuration/
.. |pypi-version| image:: https://img.shields.io/pypi/v/django-setup-configuration.svg
:target: https://pypi.org/project/django-setup-configuration/
17 changes: 10 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ build-backend = "setuptools.build_meta"
[project]
name = "django_setup_configuration"
version = "0.1.0"
description = "TODO"
description = "Pluggable configuration setup used with the django management command"
authors = [
{name = "Maykin Media", email = "[email protected]"}
]
readme = "README.rst"
license = {file = "LICENSE"}
keywords = ["TODO"]
keywords = ["Django", "Configuration"]
classifiers = [
"Development Status :: 3 - Alpha",
"Framework :: Django",
"Framework :: Django :: 3.2",
"Framework :: Django :: 4.2",
"Intended Audience :: Developers",
"Operating System :: Unix",
Expand All @@ -27,19 +28,21 @@ classifiers = [
]
requires-python = ">=3.10"
dependencies = [
"django>=4.2"
"django>=3.2"
]

[project.urls]
Homepage = "https://github.com/maykinmedia/django_setup_configuration"
Documentation = "http://django_setup_configuration.readthedocs.io/en/latest/"
"Bug Tracker" = "https://github.com/maykinmedia/django_setup_configuration/issues"
"Source Code" = "https://github.com/maykinmedia/django_setup_configuration"
Homepage = "https://github.com/maykinmedia/django-setup-configuration"
Documentation = "http://django-setup-configuration.readthedocs.io/en/latest/"
"Bug Tracker" = "https://github.com/maykinmedia/django-setup-configuration/issues"
"Source Code" = "https://github.com/maykinmedia/django-setup-configuration"

[project.optional-dependencies]
tests = [
"pytest",
"pytest-django",
"pytest-mock",
"furl",
"tox",
"isort",
"black",
Expand Down
Loading

0 comments on commit e4559f7

Please sign in to comment.