diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed24fba --- /dev/null +++ b/.gitignore @@ -0,0 +1,232 @@ +# ---> JetBrains +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# ---> JetBrainsWorkspace +# Additional coverage for JetBrains IDEs workspace files +.idea/deployment.xml +.idea/misc.xml +.idea/remote-mappings.xml +.idea/*.iml + +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..18a3ac4 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,35 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..c88a93e --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/remote-mappings.xml b/.idea/remote-mappings.xml new file mode 100644 index 0000000..dc5f417 --- /dev/null +++ b/.idea/remote-mappings.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..4c6280e --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/python-naive/README.md b/python-naive/README.md new file mode 100644 index 0000000..0aa758f --- /dev/null +++ b/python-naive/README.md @@ -0,0 +1,56 @@ +# Naive Python template + +## Run it + +Execute this template like so: +``` +cookiecutter https://quico.space/Quico/py-cookiecutter-templates.git --directory 'python-naive' +``` + +Cookiecutter interactively prompts you for the following info, here with example answers: +``` +project_slug [project-slug]: dockerhost-firewalld-update +Select rich_logging: +1 - yes +2 - no +Choose from 1, 2 [1]: +Select uses_config_ini: +1 - yes +2 - no +Choose from 1, 2 [1]: +``` + +Done, directory structure and files for your next `docker-compose` project are ready for you to hit the ground running. + +## Explanation and terminology + +Your three answers translate as follows into rendered files. + +1. The `project_slug` is used as a directory name for your Python project where spaces and underscores are replaced-with-dashes. It's also used for a few example variables where `we_use_underscores` instead. + ``` + . + └── dockerhost-firewalld-update +   ├── dockerhost-firewalld-update.py +   ├── examples +   │   └── config.ini.example +   ├── requirements.in +   └── requirements.txt + ``` +2. The `rich_logging` variable adds settings and examples that make ample use of the [Rich package](https://github.com/Textualize/rich/) for beautiful logging. You typically want this so it defaults to `yes`. Just hit `Enter` to confirm. The setting also adds necessary requirements. +3. With `uses_config_ini` you're getting a boat load of functions, presets, variables and examples that integrate a config.ini file via the `configparser` module. + +## Result + +### Enable Rich and configparser + +Above example of a Python project with Rich and `configparser` enabled will give you a directory structure like this: +``` +. +└── dockerhost-firewalld-update +   ├── dockerhost-firewalld-update.py +   ├── examples +   │   └── config.ini.example +   ├── requirements.in +   └── requirements.txt +``` +You can see real-life example file content over at [examples/rich-and-config](examples/rich-and-config). Cookiecutter has generated all necessary dependencies with pinned versions and a `rich-and-config.py` script file to get you started. diff --git a/python-naive/cookiecutter.json b/python-naive/cookiecutter.json new file mode 100644 index 0000000..60ae11a --- /dev/null +++ b/python-naive/cookiecutter.json @@ -0,0 +1,7 @@ +{ + "project_slug": "project-slug", + "__project_slug": "{{ cookiecutter.project_slug.lower().replace(' ', '-').replace('_', '-') }}", + "__project_slug_under": "{{ cookiecutter.project_slug.lower().replace(' ', '_').replace('-', '_') }}", + "rich_logging": ["yes", "no"], + "uses_config_ini": ["yes", "no"] +} diff --git a/python-naive/examples/rich-and-config/examples/config.ini.example b/python-naive/examples/rich-and-config/examples/config.ini.example new file mode 100644 index 0000000..9ed14a3 --- /dev/null +++ b/python-naive/examples/rich-and-config/examples/config.ini.example @@ -0,0 +1,18 @@ +[DEFAULT] +self_name = rich-and-config +tmp_base_dir = /tmp/%(self_name)s +state_base_dir = /var/lib/%(self_name)s +state_files_dir = %(state_base_dir)s/state +state_file_retention = 50 +state_file_name_prefix = state- +state_file_name_suffix = .log +rich_and_config_some_option = "http://localhost:8000/api/query" +another_option = "first" + +[this-is-a-section] +min_duration = 1200 +max_duration = 3000 +title_not_regex = this|that|somethingelse +query = @http-payload.json +dl_dir = /tmp/some/dir +another_option = "overwriting_from_default" diff --git a/python-naive/examples/rich-and-config/requirements.in b/python-naive/examples/rich-and-config/requirements.in new file mode 100644 index 0000000..3f382dd --- /dev/null +++ b/python-naive/examples/rich-and-config/requirements.in @@ -0,0 +1 @@ +rich diff --git a/python-naive/examples/rich-and-config/requirements.txt b/python-naive/examples/rich-and-config/requirements.txt new file mode 100644 index 0000000..369262d --- /dev/null +++ b/python-naive/examples/rich-and-config/requirements.txt @@ -0,0 +1,12 @@ +# +# This file is autogenerated by pip-compile with python 3.10 +# To update, run: +# +# pip-compile +# +commonmark==0.9.1 + # via rich +pygments==2.12.0 + # via rich +rich==12.4.4 + # via -r requirements.in diff --git a/python-naive/examples/rich-and-config/rich-and-config.py b/python-naive/examples/rich-and-config/rich-and-config.py new file mode 100644 index 0000000..724e811 --- /dev/null +++ b/python-naive/examples/rich-and-config/rich-and-config.py @@ -0,0 +1,167 @@ +import os +import configparser +import sys +import logging +from rich.logging import RichHandler + + +# Exit codes +# 1: Config file invalid, it has no sections +# 2: Config file invalid, sections must define at least CONST.CFG_MANDATORY + + +class CONST(object): + __slots__ = () + LOG_FORMAT = "%(message)s" + # How to find a config file + CFG_THIS_FILE_DIRNAME = os.path.dirname(__file__) + CFG_DEFAULT_FILENAME = "config.ini" + CFG_DEFAULT_ABS_PATH = os.path.join(CFG_THIS_FILE_DIRNAME, CFG_DEFAULT_FILENAME) + # Values you don't have to set, these are their internal defaults + CFG_KNOWN_DEFAULTS = [ + {"key": "self_name", "value": "rich-and-config"}, + {"key": "tmp_base_dir", "value": os.path.join(CFG_THIS_FILE_DIRNAME, "data/tmp/%(self_name)s")}, + {"key": "state_base_dir", "value": os.path.join(CFG_THIS_FILE_DIRNAME, "data/var/lib/%(self_name)s")}, + {"key": "state_files_dir", "value": "%(state_base_dir)s/state"}, + {"key": "state_file_retention", "value": "50"}, + {"key": "state_file_name_prefix", "value": "state-"}, + {"key": "state_file_name_suffix", "value": ".log"}, + {"key": "rich_and_config_some_option", "value": "http://localhost:8000/api/query"}, + {"key": "another_option", "value": "first"} + ] + # In all sections other than 'default' the following settings are known and accepted. We silently ignore other + # settings. We use 'is_mandatory' to determine if we have to raise errors on missing settings. + CFG_KNOWN_SECTION = [ + {"key": "min_duration", "is_mandatory": False}, + {"key": "max_duration", "is_mandatory": False}, + {"key": "title_not_regex", "is_mandatory": False}, + {"key": "query", "is_mandatory": True}, + {"key": "dl_dir", "is_mandatory": True} + ] + CFG_MANDATORY = [section_cfg["key"] for section_cfg in CFG_KNOWN_SECTION if section_cfg["is_mandatory"]] + + +logging.basicConfig( + # Default for all modules is NOTSET so log everything + level="NOTSET", + format=CONST.LOG_FORMAT, + datefmt="[%X]", + handlers=[RichHandler( + rich_tracebacks=True + )] +) +log = logging.getLogger("rich") +# Our own code logs with this level +log.setLevel(logging.DEBUG) + + +# Use this version of class ConfigParser to log.debug contents of our config file. When parsing sections other than +# 'default' we don't want to reprint defaults over and over again. This custom class achieves that. +class ConfigParser( + configparser.ConfigParser): + """Can get options() without defaults + + Taken from https://stackoverflow.com/a/12600066. + """ + + def options(self, section, no_defaults=False, **kwargs): + if no_defaults: + try: + return list(self._sections[section].keys()) + except KeyError: + raise configparser.NoSectionError(section) + else: + return super().options(section) + + +ini_defaults = [] +internal_defaults = {default["key"]: default["value"] for default in CONST.CFG_KNOWN_DEFAULTS} +config = ConfigParser(defaults=internal_defaults) +config.read(CONST.CFG_DEFAULT_ABS_PATH) + + +def print_section_header( + header: str) -> str: + return f"Loading config section '[{header}]' ..." + + +def validate_default_section( + config_obj: configparser.ConfigParser()) -> None: + log.debug(f"Loading config from file '{CONST.CFG_DEFAULT_ABS_PATH}' ...") + if not config_obj.sections(): + log.debug(f"No config sections found in '{CONST.CFG_DEFAULT_ABS_PATH}'. Exiting 1 ...") + sys.exit(1) + if config.defaults(): + log.debug(f"Symbol legend:\n" + f"* Global default from section '[{config_obj.default_section}]'\n" + f"~ Local option, doesn't exist in '[{config_obj.default_section}]'\n" + f"+ Local override of a value from '[{config_obj.default_section}]'\n" + f"= Local override, same value as in '[{config_obj.default_section}]'") + log.debug(print_section_header(config_obj.default_section)) + for default in config_obj.defaults(): + ini_defaults.append({default: config_obj[config_obj.default_section][default]}) + log.debug(f"* {default} = {config_obj[config_obj.default_section][default]}") + else: + log.debug(f"No defaults defined") + + +def config_has_valid_section( + config_obj: configparser.ConfigParser()) -> bool: + has_valid_section = False + for config_obj_section in config_obj.sections(): + if set(CONST.CFG_MANDATORY).issubset(config_obj.options(config_obj_section)): + has_valid_section = True + break + return has_valid_section + + +def is_default( + config_key: str) -> bool: + return any(config_key in ini_default for ini_default in ini_defaults) + + +def is_same_as_default( + config_kv_pair: dict) -> bool: + return config_kv_pair in ini_defaults + + +def validate_config_sections( + config_obj: configparser.ConfigParser()) -> None: + for this_section in config_obj.sections(): + log.debug(print_section_header(this_section)) + if not set(CONST.CFG_MANDATORY).issubset(config_obj.options(this_section, no_defaults=True)): + log.debug(f"Config section '[{this_section}]' does not have all mandatory options " + f"{CONST.CFG_MANDATORY} set, skipping section ...") + config_obj.remove_section(this_section) + else: + for key in config_obj.options(this_section, no_defaults=True): + kv_prefix = "~" + if is_default(key): + kv_prefix = "+" + if is_same_as_default({key: config_obj[this_section][key]}): + kv_prefix = "=" + log.debug(f"{kv_prefix} {key} = {config_obj[this_section][key]}") + + +def an_important_function( + section_name: str, + config_obj: configparser.ConfigParser(), + whatever: str) -> list: + min_duration = config_obj.getint(section_name, "min_duration") + max_duration = config_obj.getint(section_name, "max_duration") + return ["I", "am", "a", "list"] + + +if __name__ == '__main__': + validate_default_section(config) + if config_has_valid_section(config): + validate_config_sections(config) + else: + log.debug(f"No valid config section found. A valid config section has at least the mandatory options " + f"{CONST.CFG_MANDATORY} set. Exiting 2 ...") + sys.exit(2) + + log.debug(f"Iterating over config sections ...") + for section in config.sections(): + log.debug(f"Processing section '[{section}]' ...") + # ...config.ini.example diff --git a/python-naive/hooks/post_gen_project.py b/python-naive/hooks/post_gen_project.py new file mode 100644 index 0000000..7017f60 --- /dev/null +++ b/python-naive/hooks/post_gen_project.py @@ -0,0 +1,17 @@ +import os + +project_dir = os.getcwd() +examples_dir_name = "examples" +config_ini_file_name = "config.ini.example" +examples_dir_abs = os.path.join(project_dir, examples_dir_name) +config_ini_file_abs = os.path.join(project_dir, examples_dir_name, config_ini_file_name) + +if {% if cookiecutter.uses_config_ini == "yes" -%}False{% else -%}True{%- endif -%}: + try: + os.remove(config_ini_file_abs) + try: + os.rmdir(examples_dir_abs) + except OSError: + pass + except OSError: + pass diff --git a/python-naive/{{ cookiecutter.__project_slug }}/examples/config.ini.example b/python-naive/{{ cookiecutter.__project_slug }}/examples/config.ini.example new file mode 100644 index 0000000..9be5634 --- /dev/null +++ b/python-naive/{{ cookiecutter.__project_slug }}/examples/config.ini.example @@ -0,0 +1,18 @@ +[DEFAULT] +self_name = {{ cookiecutter.__project_slug }} +tmp_base_dir = /tmp/%(self_name)s +state_base_dir = /var/lib/%(self_name)s +state_files_dir = %(state_base_dir)s/state +state_file_retention = 50 +state_file_name_prefix = state- +state_file_name_suffix = .log +{{ cookiecutter.__project_slug_under }}_some_option = "http://localhost:8000/api/query" +another_option = "first" + +[this-is-a-section] +min_duration = 1200 +max_duration = 3000 +title_not_regex = this|that|somethingelse +query = @http-payload.json +dl_dir = /tmp/some/dir +another_option = "overwriting_from_default" diff --git a/python-naive/{{ cookiecutter.__project_slug }}/requirements.in b/python-naive/{{ cookiecutter.__project_slug }}/requirements.in new file mode 100644 index 0000000..4ba357e --- /dev/null +++ b/python-naive/{{ cookiecutter.__project_slug }}/requirements.in @@ -0,0 +1,3 @@ +{%- if cookiecutter.rich_logging == "yes" -%} +rich +{% endif -%} diff --git a/python-naive/{{ cookiecutter.__project_slug }}/requirements.txt b/python-naive/{{ cookiecutter.__project_slug }}/requirements.txt new file mode 100644 index 0000000..44389d0 --- /dev/null +++ b/python-naive/{{ cookiecutter.__project_slug }}/requirements.txt @@ -0,0 +1,14 @@ +{%- if cookiecutter.rich_logging == "yes" -%} +# +# This file is autogenerated by pip-compile with python 3.10 +# To update, run: +# +# pip-compile +# +commonmark==0.9.1 + # via rich +pygments==2.12.0 + # via rich +rich==12.4.4 + # via -r requirements.in +{% endif -%} diff --git a/python-naive/{{ cookiecutter.__project_slug }}/{{ cookiecutter.__project_slug }}.py b/python-naive/{{ cookiecutter.__project_slug }}/{{ cookiecutter.__project_slug }}.py new file mode 100644 index 0000000..d1f368b --- /dev/null +++ b/python-naive/{{ cookiecutter.__project_slug }}/{{ cookiecutter.__project_slug }}.py @@ -0,0 +1,192 @@ +{% if cookiecutter.uses_config_ini == "yes" -%} +import os +import configparser +import sys +{%- endif %} +{%- if cookiecutter.rich_logging == "yes" %} +import logging +from rich.logging import RichHandler +{%- endif %} +{%- if cookiecutter.rich_logging == "yes" or cookiecutter.uses_config_ini == "yes" %} + + +# Exit codes +# 1: Config file invalid, it has no sections +# 2: Config file invalid, sections must define at least CONST.CFG_MANDATORY + + +class CONST(object): + __slots__ = () +{%- endif %} + {%- if cookiecutter.rich_logging == "yes" %} + LOG_FORMAT = "%(message)s" + {%- endif %} + {%- if cookiecutter.uses_config_ini == "yes" %} + # How to find a config file + CFG_THIS_FILE_DIRNAME = os.path.dirname(__file__) + CFG_DEFAULT_FILENAME = "config.ini" + CFG_DEFAULT_ABS_PATH = os.path.join(CFG_THIS_FILE_DIRNAME, CFG_DEFAULT_FILENAME) + # Values you don't have to set, these are their internal defaults + CFG_KNOWN_DEFAULTS = [ + {"key": "self_name", "value": "{{ cookiecutter.__project_slug }}"}, + {"key": "tmp_base_dir", "value": os.path.join(CFG_THIS_FILE_DIRNAME, "data/tmp/%(self_name)s")}, + {"key": "state_base_dir", "value": os.path.join(CFG_THIS_FILE_DIRNAME, "data/var/lib/%(self_name)s")}, + {"key": "state_files_dir", "value": "%(state_base_dir)s/state"}, + {"key": "state_file_retention", "value": "50"}, + {"key": "state_file_name_prefix", "value": "state-"}, + {"key": "state_file_name_suffix", "value": ".log"}, + {"key": "{{ cookiecutter.__project_slug_under }}_some_option", "value": "http://localhost:8000/api/query"}, + {"key": "another_option", "value": "first"} + ] + # In all sections other than 'default' the following settings are known and accepted. We silently ignore other + # settings. We use 'is_mandatory' to determine if we have to raise errors on missing settings. + CFG_KNOWN_SECTION = [ + {"key": "min_duration", "is_mandatory": False}, + {"key": "max_duration", "is_mandatory": False}, + {"key": "title_not_regex", "is_mandatory": False}, + {"key": "query", "is_mandatory": True}, + {"key": "dl_dir", "is_mandatory": True} + ] + CFG_MANDATORY = [section_cfg["key"] for section_cfg in CFG_KNOWN_SECTION if section_cfg["is_mandatory"]] + {%- endif %} +{%- if cookiecutter.rich_logging == "yes" %} + + +logging.basicConfig( + # Default for all modules is NOTSET so log everything + level="NOTSET", + format=CONST.LOG_FORMAT, + datefmt="[%X]", + handlers=[RichHandler( + rich_tracebacks=True + )] +) +log = logging.getLogger("rich") +# Our own code logs with this level +log.setLevel(logging.DEBUG) +{%- endif %} +{%- if cookiecutter.uses_config_ini == "yes" %} + + +# Use this version of class ConfigParser to {% if cookiecutter.rich_logging == "yes" -%}log.debug{%- else -%}print{%- endif %} contents of our config file. When parsing sections other than +# 'default' we don't want to reprint defaults over and over again. This custom class achieves that. +class ConfigParser( + configparser.ConfigParser): + """Can get options() without defaults + + Taken from https://stackoverflow.com/a/12600066. + """ + + def options(self, section, no_defaults=False, **kwargs): + if no_defaults: + try: + return list(self._sections[section].keys()) + except KeyError: + raise configparser.NoSectionError(section) + else: + return super().options(section) + + +ini_defaults = [] +internal_defaults = {default["key"]: default["value"] for default in CONST.CFG_KNOWN_DEFAULTS} +config = ConfigParser(defaults=internal_defaults) +config.read(CONST.CFG_DEFAULT_ABS_PATH) + + +def print_section_header( + header: str) -> str: + return f"Loading config section '[{header}]' ..." + + +def validate_default_section( + config_obj: configparser.ConfigParser()) -> None: + {% if cookiecutter.rich_logging == "yes" -%}log.debug{%- else -%}print{%- endif %}(f"Loading config from file '{CONST.CFG_DEFAULT_ABS_PATH}' ...") + if not config_obj.sections(): + {% if cookiecutter.rich_logging == "yes" -%}log.debug{%- else -%}print{%- endif %}(f"No config sections found in '{CONST.CFG_DEFAULT_ABS_PATH}'. Exiting 1 ...") + sys.exit(1) + if config.defaults(): + {% if cookiecutter.rich_logging == "yes" -%}log.debug{%- else -%}print{%- endif %}(f"Symbol legend:\n" + {% if cookiecutter.rich_logging == "yes" %} {% endif %}f"* Global default from section '[{config_obj.default_section}]'\n" + {% if cookiecutter.rich_logging == "yes" %} {% endif %}f"~ Local option, doesn't exist in '[{config_obj.default_section}]'\n" + {% if cookiecutter.rich_logging == "yes" %} {% endif %}f"+ Local override of a value from '[{config_obj.default_section}]'\n" + {% if cookiecutter.rich_logging == "yes" %} {% endif %}f"= Local override, same value as in '[{config_obj.default_section}]'") + {% if cookiecutter.rich_logging == "yes" -%}log.debug{%- else -%}print{%- endif %}(print_section_header(config_obj.default_section)) + for default in config_obj.defaults(): + ini_defaults.append({default: config_obj[config_obj.default_section][default]}) + {% if cookiecutter.rich_logging == "yes" -%}log.debug{%- else -%}print{%- endif %}(f"* {default} = {config_obj[config_obj.default_section][default]}") + else: + {% if cookiecutter.rich_logging == "yes" -%}log.debug{%- else -%}print{%- endif %}(f"No defaults defined") + + +def config_has_valid_section( + config_obj: configparser.ConfigParser()) -> bool: + has_valid_section = False + for config_obj_section in config_obj.sections(): + if set(CONST.CFG_MANDATORY).issubset(config_obj.options(config_obj_section)): + has_valid_section = True + break + return has_valid_section + + +def is_default( + config_key: str) -> bool: + return any(config_key in ini_default for ini_default in ini_defaults) + + +def is_same_as_default( + config_kv_pair: dict) -> bool: + return config_kv_pair in ini_defaults + + +def validate_config_sections( + config_obj: configparser.ConfigParser()) -> None: + for this_section in config_obj.sections(): + {% if cookiecutter.rich_logging == "yes" -%}log.debug{%- else -%}print{%- endif %}(print_section_header(this_section)) + if not set(CONST.CFG_MANDATORY).issubset(config_obj.options(this_section, no_defaults=True)): + {% if cookiecutter.rich_logging == "yes" -%}log.debug{%- else -%}print{%- endif %}(f"Config section '[{this_section}]' does not have all mandatory options " + {% if cookiecutter.rich_logging == "yes" %} {% endif %}f"{CONST.CFG_MANDATORY} set, skipping section ...") + config_obj.remove_section(this_section) + else: + for key in config_obj.options(this_section, no_defaults=True): + kv_prefix = "~" + if is_default(key): + kv_prefix = "+" + if is_same_as_default({key: config_obj[this_section][key]}): + kv_prefix = "=" + {% if cookiecutter.rich_logging == "yes" -%}log.debug{%- else -%}print{%- endif %}(f"{kv_prefix} {key} = {config_obj[this_section][key]}") +{%- endif %} + + +def an_important_function( + section_name: str, +{%- if cookiecutter.uses_config_ini == "yes" %} + config_obj: configparser.ConfigParser(), +{%- endif %} + whatever: str) -> list: + {%- if cookiecutter.uses_config_ini == "yes" %} + min_duration = config_obj.getint(section_name, "min_duration") + max_duration = config_obj.getint(section_name, "max_duration") + {%- else %} + min_duration = 10 + max_duration = 20 + {%- endif %} + return ["I", "am", "a", "list"] + + +if __name__ == '__main__': + {% if cookiecutter.uses_config_ini == "yes" -%} + validate_default_section(config) + if config_has_valid_section(config): + validate_config_sections(config) + else: + {% if cookiecutter.rich_logging == "yes" -%}log.debug{%- else -%}print{%- endif %}(f"No valid config section found. A valid config section has at least the mandatory options " + {% if cookiecutter.rich_logging == "yes" %} {% endif %}f"{CONST.CFG_MANDATORY} set. Exiting 2 ...") + sys.exit(2) + + {% if cookiecutter.rich_logging == "yes" -%}log.debug{%- else -%}print{%- endif %}(f"Iterating over config sections ...") + for section in config.sections(): + {% if cookiecutter.rich_logging == "yes" -%}log.debug{%- else -%}print{%- endif %}(f"Processing section '[{section}]' ...") + # ... + {%- else -%} + pass + {%- endif %}