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 %}