Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

setup_notification command #2

Merged
merged 14 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to do this via a Django setting step? Why can't we directly point to the name of an envvar and skip having to define a Django setting that reads the envvar?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use django settings to set defaults when it's possible. For example we can have a default for SELECTIELIJST_API_ROOT


@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
Loading