Package: py-app-properties
Python Application PropertiesWhat is thisIt is a simple library to inject non-sensitive configurations into class variables.
Basically, it's likeBaseSettingsinpydanticlibrary but for constants injsonandyamlformats.py-app-propertiescan work with different Python types (liketuple,datetime,dataclassand so on) and recursively cast config values to them.When to useIf you deal with constants in your code, like error messages, default values for something, numeric coefficients, and so on.If you hate global variables, and you like non-python files to store static information.If you want to have an easy way to manage different constants depending on environments (liketest,dev,prod).If you like type hints and clean code.How to installTo install this library just enter:pipinstallpy-app-propertiesHow to useFor injecting values you need only the decoratorpropertiesunder a target class.
By default, the library will search a config fileapplication.ymlin the same directory
where your file with the used decorator is located, like below:project_root
|---services
|   |   email_message_service.py
|   |   application.yml
|.....Example:services/application.yml:default_text_style:size:14weight:boldfont:"TimesNewRoman"color:-128-128-128language_greetings:-language:englishtext:hello-language:germantext:hallo-language:frenchtext:bonjourwellcome_message:"{greeting}!Thankyouforregistration,{username}!"mailing_frequency:days:5hours:12services/email_message_service.py:fromtypingimportTypedDictfromdataclassesimportdataclassfromdatetimeimporttimedeltafromapp_propertiesimportproperties@dataclassclassTextStyle:size:intweight:strfont:strcolor:tuple[int,int,int]|strclassGreetingDict(TypedDict):language:strtext:str@propertiesclassEmailMessageService:default_text_style:TextStylelanguage_greetings:list[GreetingDict]wellcome_message:strmailing_frequency:timedelta|None# And using these class variables in some methods...And that's how will look an equivalent of the code above but with "hard-coded" constants, without config files and@propertiesdecorator:classEmailMessageService:default_text_style=TextStyle(size=14,weight="bold",font="Times New Roman",color=(128,128,128))language_greetings=[GreetingDict(language="english",text="hello"),GreetingDict(language="german",text="hallo"),GreetingDict(language="french",text="bonjour"),]wellcome_message="{greeting}! Thank you for registration,{username}!"mailing_frequency=timedelta(days=5,hours=12)# And using these class variables in some methods...All config values will be inserted and cast according to the type annotations once during the application or script start.
Additionally, the decorator takes such params:filename- the name of a file with config. By default, it isapplication.yml. Use a relative path with../to read the file from a parent directory;type_cast- used to know whether you want to cast config values to the field type.
By default, it'sTrue, which means values in a config file will be cast according to the type hints. All types specified in the sectionsupported typeswill be available for type casting. Also, nested types will be recursively cast.
IfFalse, type hinting is ignored, and available types are limited by a file format;override_default- used to know whether you want to override the default values of class variables. By default, it isFalse;lazy_init- used to know whether you want to set config values immediately on the application start-up or on demand ("lazily") after calling the methodinit_props(). By default, it isFalse;root- root key in the config. It's the way to create "namespaces" when you work with multiple classes but use a single config file. It could be a nested value with separation by dots, for example:# example.ymlservices:email_service:key:some valueauth_service:key:another valueclients:translation_client:key:value# and so on...fromapp_propertiesimportproperties@properties(filename="example.yml",root="services.email_service")classEmailService:key:str# will store "some value"@properties(filename="example.yml",root="services.auth_service")classAuthService:key:str# will store "another value"Different environmentsUsing this library it's easy to manage different environments and corresponding config files.
It could be done like so:importosfromapp_propertiesimportproperties@properties(filename=os.getenv("CONFIG_FILENAME","application.yml"))classSomeEnvDependingService:env_depend_var:strIn this case, you setCONFIG_FILENAME=application-dev.ymlin env variables, andpy-app-propertieswill use that file.Lazy initializationIf you want to create some dataclass instance with filled required data during init,
and then populated with config values, you can use the parameterlazy_initfor this purpose.
All file constants will be injected after calling the methodinit_props:# All definitions like in previous examples@properties(lazy_init=True)@dataclassclassEmailMessageServiceConfig:default_text_style:TextStylelanguage_greetings:list[GreetingDict]mailing_frequency:timedelta|None=Nonewellcome_message:str="some_default_message"email_config=EmailMessageServiceConfig(default_text_style=TextStyle(size=16,weight="normal",font="Arial",color="black"),language_greetings=[GreetingDict(language="english",text="hello")])# it works like a normal dataclass instanceassertemail_config.default_text_style==TextStyle(size=16,weight="normal",font="Arial",color="black")assertemail_config.mailing_frequencyisNoneassertemail_config.wellcome_message=="some_default_message"# after calling `init_props`, config values will be injected.# It also overrides all values that we set during initialize before.email_config.init_props()assertemail_config.default_text_style==TextStyle(size=14,weight="bold",font="Times New Roman",color=(128,128,128))assertemail_config.mailing_frequency==timedelta(days=5,hours=12)assertemail_config.wellcome_message==("{greeting}! Thank you for registration,{username}!")Because there are 3 sources of data (default values, values passed during initialization, and config file values),
it could be hard to understand how we can resolve this conflict.
Bellow is the table to clarify the behavior of theinit_propsmethod.initdefaultconfigwill be used-+-default-++config+~-init+~+init \ config+- provided;-- missing;~- not affect.How you can see, when bothinitandconfigvalues provided, they are equally important,
but, by default,confighave higher priority and overridesinit.
If you, for some reason, don't want to override already initialized values, only defaults,
it's also possible withinit_props(override_init=False)Supported typesThe table below shows how config values (jsonsyntax example) are cast to Python types:Python typeConfig file typeConfig exampleintintstr10"10"floatfloatintstr10.510"10.5"strstr"string value"boolboolintstrtrue/false1/0"True"/"False","true"/"false"Nonenullnulldictdict{"key": "value"}listtuplesetfrozensetlist["val1", "val2"]TypedDictdict{"str_var": "value"}NamedTuplelistdict["value", 10]{"str_val": "value", "int_val": 10}dataclassdict{"str_val": "str", "int_val": 10}datetime.datetimestrintlistdict"2022-12-11T10:20:23"1670754600[2022, 12, 11, 10, 20, 23]{"year": 2022, "month": 12, "day": 11, "hour": 10, "minute": 20, "second": 23}datetime.datestrlistdict"2022-12-11"[2022, 12, 11]{"year": 2022, "month": 12, "day": 11}datetime.timestrlistdict"12:30:02"[12, 30, 2]{"hour": 12, "minute": 30, "second": 2}datetime.timedeltadict{"days": 1, "hours": 2, "minutes": 10}enum.Enumstrint"VALUE"10re.Patternstr"\w+"About contributingYou will makepy-app-propertiesbetter if you open issues or create pull requests with improvements.