Package: garlicconfig
GarlicConfigGarlicConfig is a framework that makes it easy to deal with configurations in an easy yet flexible way. The core of this package is written in C++ and this Python package wraps the native code to provide an easy access to the configuration and some extra feature on top of it. The native library allows for quick retrieval of the configurations which can be used on any platform, this wrapper, however allows for defining advanced validation and config retrieval logic. For example, you could define your own conventions for getting localized version of configs.The whole thing starts by defining the structure of your configs using models.You can define models by inheriting fromConfigModel:fromgarlicconfigimportmodelsclassDatabaseConfig(models.ConfigModel):passBy adding attributes (or properties), you can define what this model recognizes and what format should each one of these attributes have.For properties, you can useConfigField. There are a set of built-in fields:StringFieldIntegerFieldArrayFieldModelFieldBooleanFieldYou can also make your own customConfigModeland/orConfigField.for example:fromgarlicconfigimportfieldsfromgarlicconfig.exceptionsimportValidationErrorclassEvenIntegerField(IntegerField):defvalidate(self,value):ifvalue%2!=0:raiseValidationError('bad integer')defto_model_value(self,value):returnint(value)defto_garlic_value(self,value):returnstr(value)The field class above storesstrin the python dictionary representation. However, for the materialized config model,intis used. It also invalidates unaccepted values by raisingValidationError, in this case odd values.Note thatto_model_valueis responsible for converting a basic type to a more complicated python type, perhaps constructing a Python-defined class.to_garlic_valuedoes the exact opposite, it should convert the value to a basic value. Basic value is defined to be one of the following types:strintfloatdictset&listThis is primarily used for ease of encoding/decoding. By default, both of these methods return the givenvaluewithout making any changes and I'd recommend not creating very complicated objects since it'll limit the accessibility of them in different platforms should you need to support them.Next, you can define your own config model and use the customConfigFieldwe just created.for example:classSomeRandomConfig(ConfigModel):value=EvenIntegerField(nullable=False,default=2)You can usepy_valuemethod on config models to get a python dictionary with basic value types in it. This is handy to cache it in memory, or to use it for serialization.from_dictwill create a new config model from a python dictionary.Furthermore, you can usegarlic_valueto construct aGarlicValuefrom the current config model and usefrom_garlicto construct a model from aGarlicValue.GarlicValueis a type that keeps configuration objects in the native code and loads them in Python lazily. This allows you to lower memory usage while speeding up all operations. It also comes with a set of handy methods:resolveAllows for requested a specific value by providing a dot separated path.for example:classParentConfig(models.ConfigModel):random_config_field=models.ModelField(SomeRandomConfig)foo=ParentConfig()foo.random_config_field.value=8garlic_value=foo.garlic_value()print(garlic_value.resolve('random_config_field.value'))In the above code, if value to the given path exists, the python representation of the value gets returned. Otherwise,Nonegets returned. This is helpful because you can simply give the path to the final value and get the requested value. Since all of this is happening in the native code, it's significantly faster to useGarlicValueover python dictionary or regular models.Your goal should be to validate models usingConfigModeland store/read configurations usingGarlicValue.cloneCopy operations, specially deep copies in Python are very expensive. You can, however, cloneGarlicValueinstances much faster by using the native clone which copies the object without the need to use deep copy yet accomplish the same result.for example:garlic_value_1=foo.garlic_value()garlic_value_2=foo.clone()SerializationYou can use the following code to encode/decode configs. The default encoder is Json. However, you can write your own encoder and support other formats as needed.fromgarlicconfigimportencodingconfig=DatabaseConfig()serialized_string=encoding.encode(config,pretty=True)Merging layersYou merge two configuration layers in order to support inheritance. Something that will come very handy if you plan to use localization or multi-layered configurations.AnyGarlicValueinstance will have aapplymethod that will basically applies a secondGarlicValueon top of itself.for example:fromgarlicconfigimportmodelsfromgarlicconfigimportfieldsclassExtraConfig(models.ConfigModel):has_id=fields.BooleanField(default=False)has_degree=fields.BooleanField(default=False)classDumbConfig(models.ConfigModel):name=fields.StringField(nullable=False)numbers=fields.ArrayField(IntegerField())extra=models.ModelField(ExtraConfig)defvalidate(self):super(DumbConfig,self).validate()ifnotself.nameandnotself.numbers:raisegarlicconfig.exceptions.ValidationError('invalid config for some reason!')config_1=DumbConfig.from_dict({'name':'Peyman','numbers':[1,2,3]'extra':{'has_id':True}}).garlic_value()config_2=DumbConfig.from_dict({'name':'Patrick','numbers':[4,5,6]'extra':{'has_degree':True}}).garlic_value()config_1.apply(config_2)config_1.resolve('numbers')# returns [4, 5, 6]config_1.resolve('name')# returns 'Patrick'config_1.resolve('extra.has_id')# returns True (from config_1)config_1.resolve('extra.has_degree')# returns True (from config_2)