From b88866f9152f4a6b16c24d938c3942e2b7f864f9 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Tue, 14 May 2024 14:59:57 +0200 Subject: [PATCH] :sparkles: Add management command to generate docs for envvars --- open_api_framework/conf/base.py | 1 + open_api_framework/conf/utils.py | 42 +++++++++++++++++-- open_api_framework/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../commands/generate_envvar_docs.py | 27 ++++++++++++ .../open_api_framework/env_config.rst | 18 ++++++++ open_api_framework/templatetags/__init__.py | 0 open_api_framework/templatetags/doc_tags.py | 35 ++++++++++++++++ 8 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 open_api_framework/management/__init__.py create mode 100644 open_api_framework/management/commands/__init__.py create mode 100644 open_api_framework/management/commands/generate_envvar_docs.py create mode 100644 open_api_framework/templates/open_api_framework/env_config.rst create mode 100644 open_api_framework/templatetags/__init__.py create mode 100644 open_api_framework/templatetags/doc_tags.py diff --git a/open_api_framework/conf/base.py b/open_api_framework/conf/base.py index 3a626af..df97be4 100644 --- a/open_api_framework/conf/base.py +++ b/open_api_framework/conf/base.py @@ -143,6 +143,7 @@ "mozilla_django_oidc_db", "log_outgoing_requests", "django_setup_configuration", + "open_api_framework", ] MIDDLEWARE = [ diff --git a/open_api_framework/conf/utils.py b/open_api_framework/conf/utils.py index 167aa05..2ac4f98 100644 --- a/open_api_framework/conf/utils.py +++ b/open_api_framework/conf/utils.py @@ -1,13 +1,42 @@ import sys +from dataclasses import dataclass from pathlib import Path -from typing import Any +from typing import Any, Optional from urllib.parse import urlparse -from decouple import Csv, config as _config, undefined +from decouple import Csv, Undefined, config as _config, undefined from sentry_sdk.integrations import DidNotEnable, django, redis -def config(option: str, default: Any = undefined, *args, **kwargs): +@dataclass +class EnvironmentVariable: + name: str + default: Any + help_text: str + group: Optional[str] = None + + def __post_init__(self): + if not self.group: + self.group = ( + "Required" if isinstance(self.default, Undefined) else "Optional" + ) + + def __eq__(self, other): + return isinstance(other, EnvironmentVariable) and self.name == other.name + + +ENVVAR_REGISTRY = [] + + +def config( + option: str, + default: Any = undefined, + help_text="", + group=None, + add_to_docs=True, + *args, + **kwargs, +): """ Pull a config parameter from the environment. @@ -17,6 +46,13 @@ def config(option: str, default: Any = undefined, *args, **kwargs): Pass ``split=True`` to split the comma-separated input into a list. """ + if add_to_docs: + variable = EnvironmentVariable( + name=option, default=default, help_text=help_text, group=group + ) + if variable not in ENVVAR_REGISTRY: + ENVVAR_REGISTRY.append(variable) + if "split" in kwargs: kwargs.pop("split") kwargs["cast"] = Csv() diff --git a/open_api_framework/management/__init__.py b/open_api_framework/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/open_api_framework/management/commands/__init__.py b/open_api_framework/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/open_api_framework/management/commands/generate_envvar_docs.py b/open_api_framework/management/commands/generate_envvar_docs.py new file mode 100644 index 0000000..7d3f4b0 --- /dev/null +++ b/open_api_framework/management/commands/generate_envvar_docs.py @@ -0,0 +1,27 @@ +import warnings +from collections import defaultdict + +from django.core.management.base import BaseCommand +from django.template import loader + +from open_api_framework.conf.utils import EnvironmentVariable + + +def convert_group_to_rst(variables: set[EnvironmentVariable]) -> str: + template = loader.get_template("open_api_framework/env_config.rst") + grouped_vars = defaultdict(list) + for var in variables: + if not var.help_text: + warnings.warn(f"missing help_text for environment variable {var}") + grouped_vars[var.group].append(var) + return template.render({"vars": grouped_vars.items()}) + + +class Command(BaseCommand): + help = "Generate documentation for all used envvars" + + def handle(self, *args, **options): + from open_api_framework.conf.utils import ENVVAR_REGISTRY + + with open("docs/rendered.rst", "w") as f: + f.write(convert_group_to_rst(ENVVAR_REGISTRY)) diff --git a/open_api_framework/templates/open_api_framework/env_config.rst b/open_api_framework/templates/open_api_framework/env_config.rst new file mode 100644 index 0000000..6879756 --- /dev/null +++ b/open_api_framework/templates/open_api_framework/env_config.rst @@ -0,0 +1,18 @@ +{% load doc_tags %}.. _installation_env_config: + +=================================== +Environment configuration reference +=================================== + +{% block intro %}{% endblock %} + +Available environment variables +=============================== + +{% for group_name, vars in vars %} +{{group_name}} +{{group_name|repeat_char:"="}} + +{% for var in vars %}* ``{{var.name}}``: {% if var.help_text %}{{var.help_text|safe|ensure_endswith:"."}}{% endif %}{% if not var.default|is_undefined %} Defaults to: ``{{var.default|to_str}}``{% endif %} +{% endfor %} +{% endfor %} diff --git a/open_api_framework/templatetags/__init__.py b/open_api_framework/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/open_api_framework/templatetags/doc_tags.py b/open_api_framework/templatetags/doc_tags.py new file mode 100644 index 0000000..e1791f5 --- /dev/null +++ b/open_api_framework/templatetags/doc_tags.py @@ -0,0 +1,35 @@ +from django import template + +from decouple import Undefined + +register = template.Library() + + +@register.filter(name="repeat_char") +def repeat_char(value, char="-"): + try: + length = len(value) + return char * length + except TypeError: + return "" + + +@register.filter(name="is_undefined") +def is_undefined(value): + return isinstance(value, Undefined) + + +@register.filter(name="to_str") +def to_str(value): + if value == "": + return "(empty string)" + return str(value) + + +@register.filter(name="ensure_endswith") +def ensure_endswith(value, char): + if not isinstance(value, str): + value = str(value) + if not value.endswith(char): + value += char + return value