2022-06-17 03:17:12 +02:00
{ % 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 )
2022-06-20 03:50:52 +02:00
# Values you don't have to set, these are their internal defaults. You may optionally add a key 'is_global' equal
# to either True or False. By default if left off it'll be assumed False. Script will treat values where
# 'is_global' equals True as not being overridable in a '[section]'. It's a setting that only makes sense in a
2022-07-05 18:24:51 +02:00
# global context for the entire script. An option where 'empty_ok' equals True can safely be unset or set to
# an empty string. An example config.ini file may give a sane config example value here, removing that value
# still results in a valid file.
2022-06-17 03:17:12 +02:00
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 " ) } ,
2022-06-20 03:50:52 +02:00
{ " key " : " state_files_dir " , " value " : " %(state_base_dir)s /state " , " is_global " : False } ,
{ " key " : " state_file_retention " , " value " : " 50 " , " is_global " : False } ,
{ " key " : " state_file_name_prefix " , " value " : " state- " , " is_global " : False } ,
{ " key " : " state_file_name_suffix " , " value " : " .log " , " is_global " : False } ,
{ " key " : " {{ cookiecutter.__project_slug_under }}_some_option " , " value " : " http://localhost:8000/api/query " , " is_global " : True } ,
{ " key " : " another_option " , " value " : " first " , " is_global " : True }
2022-06-17 03:17:12 +02:00
]
2022-07-05 18:24:51 +02:00
# In all sections other than 'default' the following settings are known and accepted. We ignore other settings.
# Per CFG_KNOWN_DEFAULTS above most '[DEFAULT]' options are accepted by virtue of being defaults and overridable.
# The only exception are options where "is_global" equals True, they can't be overridden in '[sections]'; any
# attempt at doing it anyway will be ignored. The main purpose of this list is to name settings that do not have
# a default value but can - if set - influence how a '[section]' behaves. Repeating a '[DEFAULT]' here does not
# make sense. We use 'is_mandatory' to determine if we have to raise errors on missing settings. Here
# 'is_mandatory' means the setting must be given in a '[section]'. It may be empty.
2022-06-17 03:17:12 +02:00
CFG_KNOWN_SECTION = [
2022-07-05 18:26:00 +02:00
# {"key": "an_option", "is_mandatory": True},
# {"key": "another_one", "is_mandatory": False}
2022-06-17 03:17:12 +02:00
]
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 }
2022-06-20 03:50:52 +02:00
internal_globals = [ default [ " key " ] for default in CONST . CFG_KNOWN_DEFAULTS if default [ " is_global " ] ]
2022-06-17 03:17:12 +02:00
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 "
2022-06-20 03:50:52 +02:00
{ % if cookiecutter . rich_logging == " yes " % } { % endif % } f " * Default from section ' [ { config_obj . default_section } ] ' \n "
{ % if cookiecutter . rich_logging == " yes " % } { % endif % } f " : Global option from ' [ { config_obj . default_section } ] ' , can not be overridden in local sections \n "
2022-06-17 03:17:12 +02:00
{ % 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 "
2022-06-20 03:50:52 +02:00
{ % if cookiecutter . rich_logging == " yes " % } { % endif % } f " = Local override, same value as in ' [ { config_obj . default_section } ] ' \n "
{ % if cookiecutter . rich_logging == " yes " % } { % endif % } f " # Local attempt at overriding a global, will be ignored " )
2022-06-17 03:17:12 +02:00
{ % 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 ] } )
2022-06-20 03:50:52 +02:00
if default in internal_globals :
{ % 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 " * { default } = { config_obj [ config_obj . default_section ] [ default ] } " )
2022-06-17 03:17:12 +02:00
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 )
2022-06-20 03:50:52 +02:00
def is_global (
config_key : str ) - > bool :
return config_key in internal_globals
2022-06-17 03:17:12 +02:00
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 = " ~ "
2022-06-20 03:50:52 +02:00
remove_from_section = False
if is_global ( key ) :
kv_prefix = " # "
remove_from_section = True
elif is_default ( key ) :
2022-06-17 03:17:12 +02:00
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 ] } " )
2022-06-20 03:50:52 +02:00
if remove_from_section :
config_obj . remove_option ( this_section , key )
2022-06-17 03:17:12 +02:00
{ % - 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 % }