From caf5494cbbd709121b5e27694caf7a4a43520d8e Mon Sep 17 00:00:00 2001 From: Conor Holden Date: Tue, 17 Sep 2024 15:27:17 +0200 Subject: [PATCH 1/4] :sparkles:[#64] add subgroup to generate_envvar_docs --- open_api_framework/conf/utils.py | 4 ++++ .../management/commands/generate_envvar_docs.py | 12 +++++++++--- .../templates/open_api_framework/env_config.rst | 11 +++++++++-- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/open_api_framework/conf/utils.py b/open_api_framework/conf/utils.py index 323fcf1..ed425a9 100644 --- a/open_api_framework/conf/utils.py +++ b/open_api_framework/conf/utils.py @@ -14,6 +14,7 @@ class EnvironmentVariable: default: Any help_text: str group: Optional[str] = None + sub_group: Optional[str] = None auto_display_default: bool = True def __post_init__(self): @@ -34,6 +35,7 @@ def config( default: Any = undefined, help_text="", group=None, + sub_group=None, add_to_docs=True, auto_display_default=True, *args, @@ -56,6 +58,7 @@ def config( :param help_text: The help text to be displayed for this variable in the documentation. Default `""` :param group: The name of the section under which this variable will be grouped. Default ``None`` + :param sub_group: The name of the subsection under which this variable will be grouped. Default ``None`` :param add_to_docs: Whether or not this variable will be displayed in the documentation. Default ``True`` :param auto_display_default: Whether or not the passed ``default`` value is displayed in the docs, this can be set to ``False`` in case a default needs more explanation that can be added to the ``help_text`` @@ -67,6 +70,7 @@ def config( default=default, help_text=help_text, group=group, + sub_group=sub_group, auto_display_default=auto_display_default, ) if variable not in ENVVAR_REGISTRY: diff --git a/open_api_framework/management/commands/generate_envvar_docs.py b/open_api_framework/management/commands/generate_envvar_docs.py index b31de3a..85c6b11 100644 --- a/open_api_framework/management/commands/generate_envvar_docs.py +++ b/open_api_framework/management/commands/generate_envvar_docs.py @@ -9,12 +9,16 @@ def convert_variables_to_rst(variables: list[EnvironmentVariable]) -> str: template = loader.get_template("open_api_framework/env_config.rst") - grouped_vars = defaultdict(list) + grouped_vars = defaultdict(lambda: 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()}) + grouped_vars[var.group][var.sub_group].append(var) + + vars = [] + for group, group_vars in grouped_vars.items(): + vars.append((group, group_vars.items())) + return template.render({"vars": vars}) class Command(BaseCommand): @@ -47,6 +51,8 @@ def _sort(envvar): return 0 case "Optional": return 2 + case "Setup Configuration": + return 3 case _: return 1 diff --git a/open_api_framework/templates/open_api_framework/env_config.rst b/open_api_framework/templates/open_api_framework/env_config.rst index 46b74b4..6f67f1f 100644 --- a/open_api_framework/templates/open_api_framework/env_config.rst +++ b/open_api_framework/templates/open_api_framework/env_config.rst @@ -9,11 +9,18 @@ Environment configuration reference Available environment variables =============================== -{% for group_name, vars in vars %} +{% for group_name, group_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 var.auto_display_default and not var.default|is_undefined %} Defaults to: ``{{var.default|to_str|safe}}``.{% endif %} +{% for subgroup_name, subgroup_vars in group_vars %} +{% if subgroup_name is not null %} +{{subgroup_name}} +{{subgroup_name|repeat_char:"^"}} +{% endif %} + +{% for var in subgroup_vars %}* ``{{var.name}}``: {% if var.help_text %}{{var.help_text|safe|ensure_endswith:"."}}{% endif %}{% if var.auto_display_default and not var.default|is_undefined %} Defaults to: ``{{var.default|to_str|safe}}``.{% endif %} +{% endfor %} {% endfor %} {% endfor %} From 0c76f52962f18b6db64f5bb278cea75ace68ad90 Mon Sep 17 00:00:00 2001 From: Conor Holden Date: Tue, 17 Sep 2024 16:30:05 +0200 Subject: [PATCH 2/4] :white_check_mark:[#64] fix tests for sub groups --- .../templates/open_api_framework/env_config.rst | 8 ++------ testapp/settings.py | 8 ++++++++ tests/test_generate_envvar_docs.py | 9 +++++++++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/open_api_framework/templates/open_api_framework/env_config.rst b/open_api_framework/templates/open_api_framework/env_config.rst index 6f67f1f..6847434 100644 --- a/open_api_framework/templates/open_api_framework/env_config.rst +++ b/open_api_framework/templates/open_api_framework/env_config.rst @@ -11,17 +11,13 @@ Available environment variables {% for group_name, group_vars in vars %} {{group_name}} -{{group_name|repeat_char:"-"}} - -{% for subgroup_name, subgroup_vars in group_vars %} +{{group_name|repeat_char:"-"}}{% for subgroup_name, subgroup_vars in group_vars %} {% if subgroup_name is not null %} {{subgroup_name}} {{subgroup_name|repeat_char:"^"}} {% endif %} - {% for var in subgroup_vars %}* ``{{var.name}}``: {% if var.help_text %}{{var.help_text|safe|ensure_endswith:"."}}{% endif %}{% if var.auto_display_default and not var.default|is_undefined %} Defaults to: ``{{var.default|to_str|safe}}``.{% endif %} -{% endfor %} -{% endfor %} +{% endfor %}{% endfor %} {% endfor %} {% block extra %}{% endblock %} diff --git a/testapp/settings.py b/testapp/settings.py index 9aecf64..4beac76 100644 --- a/testapp/settings.py +++ b/testapp/settings.py @@ -75,6 +75,14 @@ ROOT_URLCONF = "testapp.urls" +NOTIF_CONFIG_ENABLE = config( + "NOTIF_CONFIG_ENABLE", + default=False, + group="Setup Configuration", + sub_group="Notification Configuration", + help_text="Enable Notification Configuration", +) + # These are excluded from generate_envvar_docs test by their group VARIABLE_TO_BE_EXCLUDED = config("VARIABLE_TO_BE_EXCLUDED1", "foo", group="Excluded") VARIABLE_TO_BE_EXCLUDED = config("VARIABLE_TO_BE_EXCLUDED2", "bar", group="Excluded") diff --git a/tests/test_generate_envvar_docs.py b/tests/test_generate_envvar_docs.py index 62d69b0..fcc66bc 100644 --- a/tests/test_generate_envvar_docs.py +++ b/tests/test_generate_envvar_docs.py @@ -31,6 +31,15 @@ Defaults to the inverse of ``DEBUG``. +Setup Configuration +------------------- + +Notification Configuration +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* ``NOTIF_CONFIG_ENABLE``: Enable Notification Configuration. Defaults to: ``False``. + + From 3a07693df2410dc2ff8ad9553c6489dc7b75d1e8 Mon Sep 17 00:00:00 2001 From: Conor Holden Date: Tue, 1 Oct 2024 14:30:18 +0200 Subject: [PATCH 3/4] :sparkles:[#64] add setup config documentation command --- .../commands/generate_envvar_docs.py | 47 ++++++++++++++++--- pyproject.toml | 2 +- tests/test_generate_envvar_docs.py | 22 +++++---- 3 files changed, 54 insertions(+), 17 deletions(-) diff --git a/open_api_framework/management/commands/generate_envvar_docs.py b/open_api_framework/management/commands/generate_envvar_docs.py index 85c6b11..fafaa1a 100644 --- a/open_api_framework/management/commands/generate_envvar_docs.py +++ b/open_api_framework/management/commands/generate_envvar_docs.py @@ -1,8 +1,14 @@ import warnings from collections import defaultdict +from django.conf import settings from django.core.management.base import BaseCommand from django.template import loader +from django.utils.module_loading import import_string + +from django_setup_configuration.management.commands.generate_config_docs import ( + ConfigDocBase, +) from open_api_framework.conf.utils import EnvironmentVariable @@ -21,18 +27,24 @@ def convert_variables_to_rst(variables: list[EnvironmentVariable]) -> str: return template.render({"vars": vars}) -class Command(BaseCommand): +class Command(ConfigDocBase, BaseCommand): help = "Generate documentation for all used envvars" def add_arguments(self, parser): super().add_arguments(parser) parser.add_argument( - "--file", - help="Name and path of the file to which the documentation will be written.", + "--envvar-file", + help="Name and path of the file to which the envvar documentation will be written.", nargs="?", default="docs/env_config.rst", ) + parser.add_argument( + "--config-file", + help="Name and path of the file to which the setup configuration documentation will be written.", + nargs="?", + default="docs/setup_config.rst", + ) parser.add_argument( "--exclude-group", help="Names of groups that should not be excluded in the generated docs.", @@ -40,9 +52,13 @@ def add_arguments(self, parser): ) def handle(self, *args, **options): + self.generate_regular_config_docs(*args, **options) + self.generate_setup_config_docs(*args, **options) + + def generate_regular_config_docs(self, *args, **options): from open_api_framework.conf.utils import ENVVAR_REGISTRY - file_path = options["file"] + file_path = options["envvar_file"] exclude_groups = options["exclude_group"] or [] def _sort(envvar): @@ -51,8 +67,6 @@ def _sort(envvar): return 0 case "Optional": return 2 - case "Setup Configuration": - return 3 case _: return 1 @@ -62,3 +76,24 @@ def _sort(envvar): ) with open(file_path, "w") as f: f.write(convert_variables_to_rst(sorted_registry)) + + def generate_setup_config_docs(self, *args, **options) -> None: + full_rendered_content = "" + + file_path = options["config_file"] + if not hasattr(settings, "SETUP_CONFIGURATION_STEPS"): + return + + for config_string in settings.SETUP_CONFIGURATION_STEPS: + config_step = import_string(config_string) + + config_settings = getattr(config_step, "config_settings", None) + if not config_settings or not config_settings.independent: + continue + + rendered_content = self.render_doc(config_settings, config_step) + full_rendered_content += rendered_content + + if len(full_rendered_content) > 0: + with open(file_path, "w") as f: + f.write(full_rendered_content) diff --git a/pyproject.toml b/pyproject.toml index 3e23499..3457c9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ dependencies = [ "celery>=5.4.0", "flower>=2.0.1", "maykin-2fa>=1.0.1", - "django-setup-configuration>=0.1.0", + "django-setup-configuration>=0.3.0", ] [project.urls] diff --git a/tests/test_generate_envvar_docs.py b/tests/test_generate_envvar_docs.py index fcc66bc..d6be4dd 100644 --- a/tests/test_generate_envvar_docs.py +++ b/tests/test_generate_envvar_docs.py @@ -22,15 +22,6 @@ ``so-secret-i-cant-believe-you-are-looking-at-this``. -Optional --------- - -* ``DEBUG``: Only set this to ``True`` on a local development environment. Various other \ -security settings are derived from this setting!. Defaults to: ``False``. -* ``IS_HTTPS``: Used to construct absolute URLs and controls a variety of security settings. \ -Defaults to the inverse of ``DEBUG``. - - Setup Configuration ------------------- @@ -40,6 +31,15 @@ * ``NOTIF_CONFIG_ENABLE``: Enable Notification Configuration. Defaults to: ``False``. +Optional +-------- + +* ``DEBUG``: Only set this to ``True`` on a local development environment. Various other \ +security settings are derived from this setting!. Defaults to: ``False``. +* ``IS_HTTPS``: Used to construct absolute URLs and controls a variety of security settings. \ +Defaults to the inverse of ``DEBUG``. + + @@ -81,7 +81,9 @@ def test_generate_envvar_docs(): "open_api_framework.management.commands.generate_envvar_docs.open", mock_file ): call_command( - "generate_envvar_docs", file="some/file/path.txt", exclude_group="Excluded" + "generate_envvar_docs", + envvar_file="some/file/path.txt", + exclude_group="Excluded", ) mock_file.assert_called_once_with("some/file/path.txt", "w") From c8eb4dbbbcfc72223b95bfe4f4d9be4d5648e877 Mon Sep 17 00:00:00 2001 From: Conor Holden Date: Wed, 2 Oct 2024 15:06:25 +0200 Subject: [PATCH 4/4] :sparkles:[#64] add setup config rst templates --- .../commands/generate_envvar_docs.py | 156 ++++++++++++++++-- .../components/setup_config_step.rst | 26 +++ .../open_api_framework/env_config.rst | 15 +- .../open_api_framework/setup_config.rst | 16 ++ open_api_framework/templatetags/doc_tags.py | 10 ++ tox.ini | 2 +- 6 files changed, 203 insertions(+), 22 deletions(-) create mode 100644 open_api_framework/templates/open_api_framework/components/setup_config_step.rst create mode 100644 open_api_framework/templates/open_api_framework/setup_config.rst diff --git a/open_api_framework/management/commands/generate_envvar_docs.py b/open_api_framework/management/commands/generate_envvar_docs.py index fafaa1a..3cc2acc 100644 --- a/open_api_framework/management/commands/generate_envvar_docs.py +++ b/open_api_framework/management/commands/generate_envvar_docs.py @@ -6,6 +6,7 @@ from django.template import loader from django.utils.module_loading import import_string +from django_setup_configuration.config_settings import ConfigSettings from django_setup_configuration.management.commands.generate_config_docs import ( ConfigDocBase, ) @@ -27,7 +28,140 @@ def convert_variables_to_rst(variables: list[EnvironmentVariable]) -> str: return template.render({"vars": vars}) -class Command(ConfigDocBase, BaseCommand): +class SetupConfigDocs(ConfigDocBase): + + def generate_config_file(self) -> str: + + full_rendered_content = "" + + if not hasattr(settings, "SETUP_CONFIGURATION_STEPS"): + return full_rendered_content + + for config_string in settings.SETUP_CONFIGURATION_STEPS: + config_step = import_string(config_string) + + config_settings = getattr(config_step, "config_settings", None) + if not config_settings or not config_settings.independent: + continue + + rendered_content = self.render_doc(config_settings, config_step) + full_rendered_content += rendered_content + + template = loader.get_template("open_api_framework/setup_config.rst") + rendered = template.render( + {"rendered_configuration_steps": full_rendered_content} + ) + + return rendered + + def render_doc(self, config_settings, config_step) -> str: + """ + Render a `ConfigSettings` documentation template with the following variables: + 1. enable_setting + 2. required_settings + 3. optional_settings + 4. detailed_info + 5. title + """ + # 1. + enable_setting = getattr(config_settings, "enable_setting", None) + + # 2. + required_settings = [ + name for name in getattr(config_settings, "required_settings", []) + ] + + # additional settings from related configuration steps to embed + # the documentation of several steps into one + related_config_settings = [ + config for config in getattr(config_settings, "related_config_settings", []) + ] + required_settings_related = self.extract_unique_settings( + [config.required_settings for config in related_config_settings] + ) + # optional_settings_related = self.extract_unique_settings( + # [config.optional_settings for config in related_config_settings] + # ) + + required_settings.extend(required_settings_related) + required_settings.sort() + + optional_settings = config_settings.optional_settings + optional_settings.sort() + + # 4. + detailed_info = self.get_detailed_info( + config_settings, + related_config_settings, + ) + + # 5. + title = self.format_display_name(config_step.verbose_name) + + template_variables = { + "enable_setting": enable_setting, + "required_settings": required_settings, + "optional_settings": optional_settings, + "detailed_info": detailed_info, + "title": title, + } + + template = loader.get_template( + "open_api_framework/components/setup_config_step.rst" + ) + rendered = template.render(template_variables) + + return rendered + + def format_display_name(self, display_name: str) -> str: + """Underlines title with '=' to display as heading in rst file""" + + heading_bar = "-" * len(display_name) + display_name_formatted = f"{display_name}\n{heading_bar}" + return display_name_formatted + + def get_detailed_info( + self, + config_settings: ConfigSettings, + related_config_settings: list[ConfigSettings], + ) -> dict[dict[str]]: + """ + Get information about the configuration settings: + 1. from model fields associated with the `ConfigSettings` + 2. from information provided manually in the `ConfigSettings` + 3. from information provided manually in the `ConfigSettings` of related + configuration steps + """ + result = dict() + for field in config_settings.config_fields: + part = dict() + variable = config_settings.get_config_variable(field.name) + part["setting"] = field.verbose_name + part["description"] = field.description or "No description" + part["possible_values"] = field.field_description + part["default_value"] = field.default_value + + result[variable] = part + + self.add_additional_info(config_settings, result) + for config_settings in related_config_settings: + self.add_additional_info(config_settings, result) + + return result + + @staticmethod + def add_additional_info( + config_settings: ConfigSettings, result: dict[dict[str]] + ) -> None: + """Convenience/helper function to retrieve additional documentation info""" + + additional_info = config_settings.additional_info + + for key, value in additional_info.items(): + result[key] = value + + +class Command(BaseCommand): help = "Generate documentation for all used envvars" def add_arguments(self, parser): @@ -55,7 +189,8 @@ def handle(self, *args, **options): self.generate_regular_config_docs(*args, **options) self.generate_setup_config_docs(*args, **options) - def generate_regular_config_docs(self, *args, **options): + @staticmethod + def generate_regular_config_docs(*args, **options): from open_api_framework.conf.utils import ENVVAR_REGISTRY file_path = options["envvar_file"] @@ -77,22 +212,13 @@ def _sort(envvar): with open(file_path, "w") as f: f.write(convert_variables_to_rst(sorted_registry)) - def generate_setup_config_docs(self, *args, **options) -> None: - full_rendered_content = "" + @staticmethod + def generate_setup_config_docs(*args, **options) -> None: file_path = options["config_file"] - if not hasattr(settings, "SETUP_CONFIGURATION_STEPS"): - return + doc_generator = SetupConfigDocs() - for config_string in settings.SETUP_CONFIGURATION_STEPS: - config_step = import_string(config_string) - - config_settings = getattr(config_step, "config_settings", None) - if not config_settings or not config_settings.independent: - continue - - rendered_content = self.render_doc(config_settings, config_step) - full_rendered_content += rendered_content + full_rendered_content = doc_generator.generate_config_file() if len(full_rendered_content) > 0: with open(file_path, "w") as f: diff --git a/open_api_framework/templates/open_api_framework/components/setup_config_step.rst b/open_api_framework/templates/open_api_framework/components/setup_config_step.rst new file mode 100644 index 0000000..847e439 --- /dev/null +++ b/open_api_framework/templates/open_api_framework/components/setup_config_step.rst @@ -0,0 +1,26 @@ +{% load doc_tags %} +{% spaceless %} +{{ title }} + +Enable/Disable configuration: +""""""""""""""""""""""""""""" + +:: + + {{ enable_setting }} + +{% if required_settings %} +Required settings +""""""""""""""""" +{% for setting in required_settings %} +* ``{{ setting }}``: {% config_var_description setting detailed_info %} +{% endfor %}{% endif %} + +{% if optional_settings %} +Optional Settings +""""""""""""""""" +{% for setting in optional_settings %} +* ``{{ setting }}``: {% config_var_description setting detailed_info %} +{% endfor %}{% endif %} + +{% endspaceless %} diff --git a/open_api_framework/templates/open_api_framework/env_config.rst b/open_api_framework/templates/open_api_framework/env_config.rst index 6847434..5ad8c76 100644 --- a/open_api_framework/templates/open_api_framework/env_config.rst +++ b/open_api_framework/templates/open_api_framework/env_config.rst @@ -11,13 +11,16 @@ Available environment variables {% for group_name, group_vars in vars %} {{group_name}} -{{group_name|repeat_char:"-"}}{% for subgroup_name, subgroup_vars in group_vars %} -{% if subgroup_name is not null %} +{{group_name|repeat_char:"-"}} + +{% spaceless %}{% for subgroup_name, subgroup_vars in group_vars %}{% if subgroup_name is not null %} {{subgroup_name}} -{{subgroup_name|repeat_char:"^"}} -{% endif %} -{% for var in subgroup_vars %}* ``{{var.name}}``: {% if var.help_text %}{{var.help_text|safe|ensure_endswith:"."}}{% endif %}{% if var.auto_display_default and not var.default|is_undefined %} Defaults to: ``{{var.default|to_str|safe}}``.{% endif %} -{% endfor %}{% endfor %} +{{subgroup_name|repeat_char:"^"}}{% endif %} + +{% spaceless %} +{% for var in subgroup_vars %} +* ``{{var.name}}``: {% if var.help_text %}{{var.help_text|safe|ensure_endswith:"."}}{% endif %}{% if var.auto_display_default and not var.default|is_undefined %} Defaults to: ``{{var.default|to_str|safe}}``.{% endif %}{% endfor %}{% endspaceless %}{% endfor %}{% endspaceless %} + {% endfor %} {% block extra %}{% endblock %} diff --git a/open_api_framework/templates/open_api_framework/setup_config.rst b/open_api_framework/templates/open_api_framework/setup_config.rst new file mode 100644 index 0000000..8c9c1cb --- /dev/null +++ b/open_api_framework/templates/open_api_framework/setup_config.rst @@ -0,0 +1,16 @@ +{% load doc_tags %}.. _installation_setup_config: + +============================= +Setup configuration reference +============================= + +{% block intro %} +Setup configuration description +{% endblock %} + +Configuration Environment Variables +=================================== + +All configuration steps currently available: + +{{ rendered_configuration_steps | safe }} diff --git a/open_api_framework/templatetags/doc_tags.py b/open_api_framework/templatetags/doc_tags.py index e1791f5..6eba55b 100644 --- a/open_api_framework/templatetags/doc_tags.py +++ b/open_api_framework/templatetags/doc_tags.py @@ -33,3 +33,13 @@ def ensure_endswith(value, char): if not value.endswith(char): value += char return value + + +@register.simple_tag +def config_var_description(variable, details): + + var_details = details.get(variable) + if var_details: + return var_details.get("description", "No description provided") + + return "No description provided" diff --git a/tox.ini b/tox.ini index b1ea562..51b85bd 100644 --- a/tox.ini +++ b/tox.ini @@ -27,7 +27,7 @@ extras = deps = django42: Django~=4.2.0 commands = - py.test tests \ + py.test tests -vv \ --cov --cov-report xml:reports/coverage-{envname}.xml \ {posargs}