feat(naive-python): Add Cookiecutter template to create a Python project
This commit is contained in:
		| @@ -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 %} | ||||
		Reference in New Issue
	
	Block a user