Skip to main content

Config injector for Python

Project description

Python Config Injector

PyPI - Python Version PyPI - Package Version PyPI - License Build Status Coverage Status Code style: black Checked with mypy Imports: isort security: bandit

What is this

It is a simple library to inject non-sensitive configurations into class variables. Basically, it's like BaseSettings in pydantic library but for constants in json, yaml or toml formats. conjector can work with different Python types (like tuple, datetime, dataclass and so on) and recursively cast config values to them.

When to use

  • If 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 (like test, dev, prod).
  • If you like type hints and clean code.

How to install

To install this library just enter:

pip install conjector

By default, conjector work only with the builtin json deserializer. To work with yaml or toml (if you are using python <= 3.10):

pip install conjector[yaml]
# or
pip install conjector[toml]
# or faster version of json
pip install conjector[json]

How to use

For injecting values you need only the decorator properties under a target class. By default, the library will search a config file application.yml in 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: 14
  weight: bold
  font: "Times New Roman"
  color:
    - 128
    - 128
    - 128
language_greetings:
  - language: english
    text: hello
  - language: german
    text: hallo
  - language: french
    text: bonjour
wellcome_message: "{greeting}! Thank you for registration, {username}!"
mailing_frequency:
  days: 5
  hours: 12

services/email_message_service.py:

from typing import TypedDict
from dataclasses import dataclass
from datetime import timedelta
from app_properties import properties

@dataclass
class TextStyle:
    size: int
    weight: str
    font: str
    color: tuple[int, int, int] | str

class GreetingDict(TypedDict):
    language: str
    text: str

@properties
class EmailMessageService:
    default_text_style: TextStyle
    language_greetings: list[GreetingDict]
    wellcome_message: str
    mailing_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 @properties decorator:

class EmailMessageService:
    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 is application.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's True, which means values in a config file will be cast according to the type hints. All types specified in the section supported types will be available for type casting. Also, nested types will be recursively cast. If False, 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 is False;
  • 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 method init_props(). By default, it is False;
  • 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.yml
services:
  email_service:
    key: some value
  auth_service:
    key: another value

clients:
  translation_client:
    key: value

# and so on...
from app_properties import properties

@properties(filename="example.yml", root="services.email_service")
class EmailService:
    key: str  # will store "some value"


@properties(filename="example.yml", root="services.auth_service")
class AuthService:
    key: str  # will store "another value"

Different environments

Using this library it's easy to manage different environments and corresponding config files. It could be done like so:

import os
from app_properties import properties


@properties(filename=os.getenv("CONFIG_FILENAME", "application.yml"))
class SomeEnvDependingService:
    env_depend_var: str

In this case, you set CONFIG_FILENAME=application-dev.yml in env variables, and conjector will use that file.

Lazy initialization

If you want to create some dataclass instance with filled required data during init, and then populated with config values, you can use the parameter lazy_init for this purpose. All file constants will be injected after calling the method init_props:

# All definitions like in previous examples

@properties(lazy_init=True)
@dataclass
class EmailMessageServiceConfig:
    default_text_style: TextStyle
    language_greetings: list[GreetingDict]
    mailing_frequency: timedelta | None = None
    wellcome_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 instance
assert email_config.default_text_style == TextStyle(
    size=16, weight="normal", font="Arial", color="black"
)
assert email_config.mailing_frequency is None
assert email_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()
assert email_config.default_text_style == TextStyle(
    size=14, weight="bold", font="Times New Roman", color=(128, 128, 128)
)
assert email_config.mailing_frequency == timedelta(days=5, hours=12)
assert email_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 the init_props method.

init default config will be used
- + - default
- + + config
+ ~ - init
+ ~ + init \ config

+- provided; -- missing; ~- not affect.

How you can see, when both init and config values provided, they are equally important, but, by default, config have higher priority and overrides init. If you, for some reason, don't want to override already initialized values, only defaults, it's also possible with init_props(override_init=False)

Supported types

The table below shows how config values (json syntax example) are cast to Python types:

Python type Config file type Config example
int int
str
10
"10"
float float
int
str
10.5
10
"10.5"
str str "string value"
bool bool
int
str
true / false
1 / 0
"True" / "False", "true" / "false"
None null null
dict dict {"key": "value"}
list
tuple
set
frozenset
list ["val1", "val2"]
TypedDict dict {"str_var": "value"}
NamedTuple list
dict
["value", 10]
{"str_val": "value", "int_val": 10}
dataclass dict {"str_val": "str", "int_val": 10}
datetime.datetime str
int
list
dict
"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.date str
list
dict
"2022-12-11"
[2022, 12, 11]
{"year": 2022, "month": 12, "day": 11}
datetime.time str
list
dict
"12:30:02"
[12, 30, 2]
{"hour": 12, "minute": 30, "second": 2}
datetime.timedelta dict {"days": 1, "hours": 2, "minutes": 10}
enum.Enum str
int
"VALUE"
10
re.Pattern str "\w+"

Warning: toml config format doesn't support heterogeneous types in an array, like ["string", 10]. So, using iterables with mixed types (e.g. list[str | int] or tuple[str, int]) and corresponding type casting aren't possible in this case.

About contributing

You will make conjector better if you open issues or create pull requests with improvements.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

conjector-1.4.0.tar.gz (14.8 kB view details)

Uploaded Source

Built Distribution

conjector-1.4.0-py3-none-any.whl (11.4 kB view details)

Uploaded Python 3

File details

Details for the file conjector-1.4.0.tar.gz.

File metadata

  • Download URL: conjector-1.4.0.tar.gz
  • Upload date:
  • Size: 14.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.1 CPython/3.10.7

File hashes

Hashes for conjector-1.4.0.tar.gz
Algorithm Hash digest
SHA256 0e6183bc4706b6b52ad568e64c126477ca02819b32091c77aa6fc6c429d856fe
MD5 08e68293638f8578b2201378796ca192
BLAKE2b-256 51b80c216fb11e4c0511028211337e49c413b8b324fbf2de9f8417c6291d3f9b

See more details on using hashes here.

File details

Details for the file conjector-1.4.0-py3-none-any.whl.

File metadata

  • Download URL: conjector-1.4.0-py3-none-any.whl
  • Upload date:
  • Size: 11.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.1 CPython/3.10.7

File hashes

Hashes for conjector-1.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4b9809608652a1e79a774a846f3b38bd98327d6bbce362df8351d9a70b3df590
MD5 fd64b108f3ad0f1c776b5311438b5e7d
BLAKE2b-256 74ea42c6a9c6e6cae484dc7c80eaa475124c46f591097f39ed6cda19035ca783

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page