Package: levy
levyYet Another Configuration ParserThis project is a lightweight take on configuration parsing with a twist.Docs:https://pmbrull.github.io/levy/InstallationGet up and running withpipinstalllevyIt supports reading both JSON and YAML files, as well as getting configurations
directly from adict.The interesting approach here is regarding handling multiple environments. Usually we
need to pass different parameters depending on where we are (DEV, PROD, and any
arbitrary environment name we might use). It is also common to have these specific parameters
available as env variables, be it our infra or in a CI/CD process.levyadds ajinja2layer on top of our config files, so that not only we can load
env variables on the fly, but helps us leverage templating syntax to keep
our configurations centralized and DRY.How toLet's suppose we have the following configuration:title:"Lévythecat"colors:-"black"-"white"hobby:eating:what:"anything"friends:{%set friends =["cartman","lima"]%}{%for friend in friends %}-name:${ friend }type:"cat"{%endfor %}We have a bit of everything:Root configurationsSimple listsNested configurationsDynamicjinja2lists as nested configurationsWe can create ourConfigobject asfromlevy.configimportConfigcfg=Config.read_file("test.yaml")As there is thejinja2layer we might want to check what is the shape of the
parsed values. We can do so withcfg._vars. In our case we'll get back something
like:{
'title': 'Lévy the cat',
'colors': ['black', 'white'],
'hobby': {
  'eating': {
    'what': 'anything'
    }
  },
'friends': [
  {'name': 'cartman', 'type': 'cat'},
  {'name': 'lima', 'type': 'cat'}
  ]
}OBS: When reading from files and for debugging purposes, we can access thecfg._filevar to check what file was parsed.Accessing valuesAll the information has been set as attributes to theConfiginstance. We can
retrieve the values ascfg.<name>, e.g.cfg.title# 'Lévy the cat'cfg.colors# ['black', 'white']Note that so far those are justrootvalues, as they come directly from the root
configuration. Whenever we have a nested item, we are creating aConfigattribute
with the key as name:print(cfg)# Config(root)print(cfg.hobby)# Config(hobby)If we need to retrieve nested values, as we are just nestingConfiginstances, we can
keep chaining attribute calls:cfg.hobby.eating.what# 'anything'Nested Config listsThecolorslist has nothing fancy in it, as we have simple types. However, we want
to parse nested configurations asConfig, while being able to access them by name
as attributes.To fit this spot we havenamedtuples. The list attribute becomes anamedtuplewhere
the properties are thenames of the nested items.nameis set as the default
identifier, but we can pass others as parameter,print(cfg.friends.lima)# Config(lima)cfg.friends.lima.type# 'cat'And if we check the type...isinstance(cfg.friends,tuple)# TrueIf we encounter an error while defining thenamedtuples structure, we will get aListParseException. We should then check how are we defining the lists and ourlist_id.OBS: Note that thelist_idfield should be a validnamedtuplekey. This means that
it cannot contain spaces or other not supported special characters.Using defaultsIt is common to fall back to default values when some parameter is not informed in our configuration.We can__call__ourConfigin order to be able to apply them.cfg("not in there",default="default")# 'default'cfg("not in there",default=None)# NoneIf no default is specified, the call will run the usual attribute retrieval. This is
interesting for cases where we need to dynamically get some configuration thatshouldbe there:cfg("not in there")# AttributeErrorRender custom functionsEnvironment VariablesWith this templating approach on top of our files, we can not only use default behaviors, but also
define our own custom functionalities.The one we have provided by default is reading environment variables at render time:variable:${ env('VARIABLE') }default:${ env('foo', default='bar') }Where the functionenvis the key name given to a function defined togetenv vars
with an optional default. If the env variable is not found and no default is provided,
we'll get aMissingEnvException.Registering new functionsIf we need to apply different functions when rendering the files, we can register them
by name before instantiating theConfigclass.Let's imagine the following YAML file:variable:${ my_func(1) }foo:${ bar('x') }We then need to define the behavior of the functionsmy_funcandbar.fromlevy.configimportConfigfromlevy.rendererimportrender_reg@render_reg.add()# By default, it registers the function namedefmy_func(num:int):returnnum+1@render_reg.add('bar')# Name can be overwritten if requireddefupper(s:str):returns.upper()cfg=Config.read_file("<file>")cfg.variable# 2cfg.foo# 'X'Note how we registeredmy_funcwith the same name it appeared in the YAML. However,
the name is completely arbitrary, and we can pass the functionupperwith the namebar.With this approach one can add even further dynamism to both YAML and JSON config files.To peek into the registry state, we can run:render_reg.registryWhich in the example will show us{'env': <function __main__.get_env(conf_str: str, default: Optional[str] = None) -> str>,
 'my_func': <function __main__.my_func(num: int)>,
 'bar': <function __main__.upper(s: str)>}Schema ValidationAt some point it might be interesting to make sure that the config we are reading follows
some standards. That is why we have introduced the ability to pass a schema our file
needs to follow.This feature is supported byPydantic,
and not only helps us to validate the schema, but even updating the values we're
reading withOptionalsand defaults.We can get this running asfrompydanticimportBaseModelclassFriends(BaseModel):name:strtype:strfur:str="soft"classKitten(BaseModel):title:strage:Optional[int]colors:List[str]hobby:Dict[str,Dict[str,str]]friends:List[Friends]cfg=Config.read_file("<file>",datatype=Kitten)# We should have the data attribute now hosting the data classassertcfg.dataisnotNone# We have optional values as Noneassertcfg.ageisNone# We have missing values with their defaultassertcfg.friends.lima.fur=="soft"Note how this adds even another layer of flexibility, as after reading the file we will
have all the data we might require available to use.ContributingYou can install the project requirements withmake install. To run the tests,make install_testandmake unit.Withmake precommit_installyou can install the pre-commit hooks.To install the package from source, clone the repo,pip install flitand runflit install.Referencespyconfsas inspiration.pydantic- implementing the validation and
data filling.