pkonfig


Namepkonfig JSON
Version 1.2 PyPI version JSON
download
home_pagehttps://github.com/ngladkikh/pkonfig
SummaryPythonic agile application configuration helpers
upload_time2022-05-23 07:26:41
maintainerNikita Gladkikh
docs_urlNone
authorNikita Gladkikh
requires_python>=3.6
licenseMIT
keywords config configuration configurations settings env environment environments dotenv application python-config yaml toml ini
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage
            # PKonfig

__P__ stands for __Python__.

[![pypi](https://img.shields.io/pypi/v/pkonfig.svg)](https://pypi.python.org/pypi/pkonfig)
[![downloads](https://img.shields.io/pypi/dm/pkonfig)](https://pepy.tech/project/pkonfig)
[![versions](https://img.shields.io/pypi/pyversions/pkonfig.svg)](https://github.com/ngladkikh/pkonfig)
[![license](https://img.shields.io/github/license/ngladkikh/pkonfig.svg)](https://github.com/ngladkikh/pkonfig/blob/master/LICENSE)

## Prerequisites

- Pythonic configuration management helpers.
- Multiple sources of configs (environment variables, dotenv files, YAML, JSON, TOML, INI)
with agile order configuration.
- Configs validation mechanics based on type hints or user defined classes.
- Minimal external dependencies.
- Follow [Fail-fast](https://en.wikipedia.org/wiki/Fail-fast) principle.
- Autocomplete in modern IDEs.

## Features

- User defined config source order.
- Multilevel configs for environment variables and dotenv config sources.
- Custom aliases for fields or groups of configs.
- Configs type casting
- Config values validation based on type and/or value.
- High performance.
- Extendable API.

## Installation

To install basic PKonfig without YAML and TOML support run:

```bash
pip install pkonfig
```

YAML files parsing is handled with [PyYaml](https://pypi.org/project/PyYAML/):

```bash
pip install pkonfig[yaml]
```

TOML files handled with help of [Tomli](https://pypi.org/project/tomli/):

```bash
pip install pkonfig[toml]
```

And if both TOML and YAML is needed:

```bash
pip install pkonfig[toml,yaml]
```

For production no __.env__ files are needed but propper environment variables should be set.
In case some of required variables missing __KeyError__ exception raised while __AppConfig__
instantiation.

## Quickstart

The most basic usage example when environment variables are used for production
environment and DotEnv files are used for local development.

Create __config__ module __config.py__:

```python
from collections import ChainMap
from pkonfig import Config, EmbeddedConfig, Env, DotEnv, LogLevel, Choice


class PG(EmbeddedConfig):
  host = "localhost"
  port = 5432
  user: str
  password: str

  
class AppConfig(Config):
    db = PG()
    log_level = LogLevel("INFO")
    env = Choice(["local", "prod", "test"], default="prod")


storage = ChainMap(DotEnv(".env", missing_ok=True), Env())
config = AppConfig(storage)
```

For local development create DotEnv file in root app folder __.env__:

```dotenv
APP_DB_HOST=localhost
APP_DB_USER=postgres
APP_DB_PASSWORD=postgres
APP_ENV=local
APP_LOG_LEVEL=debug
```

Then elsewhere in app you could run:

```python
from config import config

print(config.env)           # local
print(config.log_level)     # 20
print(config.db.host)       # localhost
print(config.db.port)       # 5432
print(config.db.user)       # postgres
print(config.db.password)   # postgres
```

## Usage

### Config sources

__PKonfig__ implements several config sources out of the box.
All config sources implement `Mapping` protocol and default values could be set up during initialization.

#### Environment variables

The most common way to configure application is environment variables.
To parse environment variables and store values in multilevel structure class `Env` could be used.
Common pattern is naming variables with multiple words describing the exact purpose 
more precise: __PG_HOST__, __PG_PORT__ and __REDIS_HOST__, __REDIS_PORT__ could be treated as two groups:

- PG
  - HOST
  - PORT
- REDIS
  - HOST
  - PORT

PKonfig respects this convention so that `Env` has two optional arguments:

- `delimiter` string that will be used to split configuration levels taken from keys;
- `prefix` string that is used to identify keys that are related to the given app and omit everything else.

```python
from os import environ
from pkonfig import Env


environ["APP_OUTER"] = "foo"
environ["APP_INNER_KEY"] = "baz"
environ["NOPE"] = "qwe"

source = Env(delimiter="_", prefix="APP", some_key="some")

print(source["outer"])          # foo
print(source["inner"]["key"])   # baz
print(source["nope"])           # raises KeyError
```

`Env` ignores key cases and ignores all keys starting not from __prefix__.
To change this behaviour set __prefix__ to `None`.
In this case you will get all key value pairs:

```python
from os import environ
from pkonfig import Env

environ["NOPE"] = "qwe"

source = Env(prefix=None)

print(source["nope"])   # qwe
```

#### DotEnv

In the same manner as environment variables DotEnv files could be used.
`DotEnv` requires file name as a string or a path and also accepts `delimiter` and `prefix` optional arguments.
`missing_ok` argument defines whether `DotEnv` raises exception when given file not found.
When file not found and `missing_ok` is set `DotEnv` contains empty dictionary.

```python
from pkonfig import DotEnv


config_source = DotEnv("test.env", delimiter="_", prefix="APP", missing_ok=True)
```

#### Ini

__INI__ files are quite common and class `Ini` 
is build on top of [`configparser.ConfigParser`](https://docs.python.org/3/library/configparser.html):

```python
from pkonfig import Ini

storage = Ini("config.ini", missing_ok=False)
print(storage["bitbucket.org"]["User"])  # hg
print(storage["bitbucket.org"]["ServerAliveInterval"])  # 45
```

In case when __config.ini__:

```ini
[DEFAULT]
ServerAliveInterval = 45

[bitbucket.org]
User = hg
```

`Ini` also accepts `missing_ok` argument to ignore missing file.
Most of `ConfigParser` arguments are also accepted to modify parser behaviour.

#### Json

`Json` class uses `json.load` to read given JSON file and respects `missing_ok` argument:

```python
from pkonfig import Json


storage = Json("config.json", missing_ok=False)
```

#### Yaml

To parse YAML files [PyYaml](https://pyyaml.org/wiki/PyYAMLDocumentation) could be used wrapped with `Yaml` class:

```python
from pkonfig import Yaml

storage = Yaml("config.yaml", missing_ok=False)
```

#### Toml

TOML files are parsed with [tomli](https://pypi.org/project/tomli/) wrapped with `Toml` helper class:

```python
from pkonfig import Toml


storage = Toml("config.toml", missing_ok=False)
```

### Source order

Any source for `BaseConfig` should implement `Mapper` protocol.
So it is easy to implement custom or combine existing implementations.
Recommended way to combine multiple sources of configs is `ChainMap`:

```python
from collections import ChainMap
from pkonfig import Env, DotEnv, Yaml


config_source = ChainMap(
    DotEnv("test.env", missing_ok=True),
    Env(),
    Yaml("base_config.yaml")
)
```

In this example we created `ChainMap` that looks for key until finds one in the given mappers sequence.
The first one source for configs is **test.env** file that might not exist and could be used for local development only.
Environment variables are used as the second one config source.
Dotenv file will be preferred source in this example.
The last one source is **base_config.yaml** that should exist or `FileNotFoundError` exception raised.

You can customize source order in this way or even create your own logic implementing
`Mapper` protocol.

### Config

To implement application config class user should inherit from `pkonfig.config.Config` class and define
required fields:

```python
from pkonfig import Config


class AppConfig(Config):
    foo: float
    baz: int


storage = {"foo": "0.33", "baz": 1}
config = AppConfig(storage)

print(config.foo)   # 0.33
print(config.baz)   # 1
```

To build more granular config structure `EmbeddedConfig` class is used:

```python
from pkonfig import Config, EmbeddedConfig


class Inner(EmbeddedConfig):
    key: str


class AppConfig(Config):
    inner = Inner()
    foo: float
    baz: int


storage = {
    "foo": "0.33", 
    "baz": 1, 
    "inner": {"key": "value"}
}
config = AppConfig(storage)

print(config.inner.key)   # value
```

### Multilevel Config

Grouping might be useful when there are lots of config parameters.
To achieve this `EmbeddedConfig` class should be inherited:

```python
from pkonfig import DotEnv, Config, EmbeddedConfig


class PgConfig(EmbeddedConfig):
    host: str
    port: int = 5432


class RedisConfig(EmbeddedConfig):
    host: str
    port: int = 6379


class AppConfig(Config):
    pg = PgConfig()
    redis = RedisConfig()


config = AppConfig(
    DotEnv(".env", delimiter="__", prefix="APP")
)

print(config.pg.host)       # db_host
print(config.pg.port)       # 6432
print(config.redis.host)    # redis
```

__.env__ content:
```dotenv
APP__PG__HOST=db_host
APP__PG__PORT=6432
APP__REDIS__HOST=redis
```
In this example we customized delimiter with two underscores, default is '**_**'.

### Aliases

All __Config__ fields accept __alias__ argument. 
When storage class searches for config attribute in its source either attribute
name is used or alias when it is set.

__config.py__:
```python
from pkonfig import DotEnv, EmbeddedConfig, Config, Int, Str


class HostConfig(EmbeddedConfig):
    host: str
    port: int
    user: str
    password = Str(alias="pass")


class AppConfig(Config):
    pg = HostConfig(alias="db")
    foo_baz = Int(alias="my_alias")


config = AppConfig(DotEnv(".env", delimiter="__"))
```

__.env__ content:

```dotenv
APP__DB__HOST=db_host
APP__DB__PORT=6432
APP__DB__PASS=password
APP__DB__USER=postgres
APP__MY_ALIAS=123
```

In this example storage will seek in dotenv file parameters named by given alias.
Elsewhere in an app:

```python
from config import config


print(config.foo_baz)       # 123
print(config.pg.password)   # password
```

### PKonfig fields

All simple Python data types are implemented in field types: `Bool`, `Int`, `Float`, `Str`, `Byte`, `ByteArray`.
All fields with known type converted to descriptors during class creation.
Fields in `Config` classes may be defined in several ways:

#### Using types:
```python
from pathlib import Path
from pkonfig import Config


class AppConfig(Config):
    foo: str
    baz: int
    flag: bool
    file: Path
```

#### Using default values:

```python
from pathlib import Path
from pkonfig import Config


class AppConfig(Config):
    foo = "some"
    baz = 1
    flag = False
    file = Path("some.text")
```

Given values will be used as default values.

#### Using PKonfig fields directly

```python
from pkonfig import Config, PathField, Str, Int, Bool


class AppConfig(Config):
    foo = Str()
    baz = Int()
    flag = Bool()
    file = PathField()
```

#### Caching

All __PKonfig__ field types are Python descriptors that are responsible for type casting and data validation.
In most cases there is no need to do this job every time the value is accessed.
To avoid undesirable calculations caching is used.
So that type casting and validation is done only once 
during `Config` object initialization.
In case when configuration may change during application lifecycle user may disable this behaviour:

```python
from pkonfig import Config, Int


class AppConfig(Config):
    attr = Int(no_cache=True)
```

In given example `attr` will do type casting and validation every time this attribute is accessed.

#### Default values

If value is not set in config source user can use default value.
`None` could be used as default value:

```python
from pkonfig import Config, Int, Str


class AppConfig(Config):
    int_attr = Int(None)
    str_attr = Str(None)

config = AppConfig({})
print(config.str_attr)    # None
print(config.int_attr)    # None
```

When `None` is default value field is treated as nullable.

#### Field nullability

To handle type casting and validation fields should not be nullable.
In case `None` is a valid value and should be used without casting and validation
option `nullable` could be set:

```python
from pkonfig import Int, Config


class AppConfig(Config):
    int_attr = Int(nullable=True)

config = AppConfig(dict(int_attr=None))
print(config.int_attr)    # None
```

In this example `None` comes from storage and type casting is omitted.

### Custom descriptor or property

```python
from pkonfig import Config


class AppConfig(Config):
    flag = True
    baz = "test"
    
    @property
    def value(self):
        return self.flag and self.baz == "test" 


config = AppConfig({})
print(config.value)  # True
```

### Custom field types

User can customize how field validation and casting is done.
The recommended way is to implement `validate` method:

```python
from pkonfig import Config, Int


class OnlyPositive(Int):
    def validate(self, value):
        if value < 0:
            raise ValueError("Only positive values accepted")


class AppConfig(Config):
    positive = OnlyPositive()
```

Custom type casting is also available.
To achieve this user should inherit abstract class `Field` and implement method `cast`:

```python
from typing import List
from pkonfig import Field

class ListOfStrings(Field):
    def cast(self, value: str) -> List[str]:
        return value.split(",")
```

### Available fields

Builtin Python types has appropriate `Field` types:

- bool -> `Bool`
- int -> `Int`
- float -> `Float`
- Decimal -> `DecimalField`
- str -> `Str`
- bytes -> `Byte`
- bytearray -> `ByteArray`

The only reason to use this types directly is customising field nullability and cache policy.

#### PathField

Basic path type that is parental for other two types and is used when you define field using `pathlib.Path`.
This type raises `FileNotFoundError` exception during initialization if given path doesn't exist:

```python
from pkonfig import Config, PathField


class AppConfig(Config):
    mandatory_existing_path = PathField()
    optional_path = PathField(missing_ok=True)
```

In given example field `optional_path` may not exist during initialization.

##### File

`File` inherits `PathField` but also checks whether given path is a file.

#### Folder

`Folder` inherits `PathField` and does checking whether given path is a folder.

#### EnumField

This field uses custom enum to validate input and cast it to given `Enum`:

```python
from enum import Enum
from pkonfig import Config, EnumField


class UserType(Enum):
    guest = 1
    user = 2
    admin = 3


class AppConfig(Config):
    user_type = EnumField(UserType)


config = AppConfig({"user_type": "admin"})
print(config.user_type is UserType.admin)  # True
```

#### LogLevel

`LogLevel` field is useful to define `logging` level through configs.
`LogLevel` accepts strings that define log level and casts 
that string to `logging` level integer value:

```python
import logging
from pkonfig import Config, LogLevel


class AppConfig(Config):
    some_level = LogLevel()
    another_level = LogLevel()


config = AppConfig(
    {
        "some_level": "info",
        "another_level": "Debug",
    }
)

print(config.some_level)        # 20
print(config.another_level)     # 10

print(config.another_level is logging.DEBUG)     # True
```

#### Choice

`Choice` field validates that config value is a member of the given sequence and also does optional type casting:

```python
from pkonfig import Config, Choice


class AppConfig(Config):
    one_of_attr = Choice([10, 100], cast_function=int)


config = AppConfig({"one_of_attr": "10"})
print(config.one_of_attr == 10)  # True

config = AppConfig({"one_of_attr": "2"})    # raises TypeError exception
```

When `cast_function` is not given raw values from storage are used.

#### DebugFlag

`DebugFlag` helps to set widely used __debug__ option.
`DebugFlag` ignores value case and treats __'true'__ string as `True` and any other value as `False`:

```python
from pkonfig import Config, DebugFlag


class AppConfig(Config):
    lower_case = DebugFlag()
    upper_case = DebugFlag()
    random_string = DebugFlag()


config = AppConfig(
  {
    "lower_case": "true",
    "upper_case": "TRUE",
    "random_string": "foo",
  }
)
print(config.lower_case)        # True
print(config.upper_case)        # True
print(config.random_string)     # False
```

### Types to Fields mapping

All fields for `BaseConfig` children classes are converted to descriptors internally.
Class `pkonfig.config.DefaultMapper` defines how field types will be replaced with descriptors.
This mapping is used by default:
```
{
    bool: Bool,
    int: Int,
    float: Float,
    str: Str,
    bytes: Byte,
    bytearray: ByteArray,
    Path: PathField,
    Decimal: DecimalField,
}
```

When field type is not found in this mapper it is ignored and won't be taken from storage source while resolving.

User can modify default mapper giving dictionary of types and appropriate fields:

```python
from decimal import Decimal
from pkonfig import Config, DefaultMapper, DecimalField


class AppConfig(Config):
    _mapper = DefaultMapper({float: DecimalField})
    foo: float

config = AppConfig(dict(foo=1/3))
assert isinstance(config.foo, Decimal)  # True
```

### Per-environment config files

When your app is configured with different configuration files 
and each file is used only in an appropriate environment you can create a function
to find which file should be used:

```python
from pkonfig import Env, Yaml, Config, Choice


CONFIG_FILES = {
    "prod": "configs/prod.yaml",
    "staging": "configs/staging.yaml",
    "local": "configs/local.yaml",
}


def get_config_file():
    class _Config(Config):
        env = Choice(
          ["prod", "local", "staging"], 
          cast_function=str.lower,
          default="prod"
        )
    
    _config = _Config(Env())
    return CONFIG_FILES[_config.env]
```

__get_config_file__ uses environment variables and predefined config files pathes
to check whether __APP_ENV__ var is set, validate this variable and return appropriate
config file name.
Then actual application configuration:

```python
from collections import ChainMap
from pkonfig import Env, Yaml, Config, Choice


CONFIG_FILES = {
    "prod": "configs/prod.yaml",
    "staging": "configs/staging.yaml",
    "local": "configs/local.yaml",
}


def get_config_file():
    class _Config(Config):
        env = Choice(
          ["prod", "local", "staging"], 
          cast_function=str.lower,
          default="prod"
        )
    
    _config = _Config(Env())
    return CONFIG_FILES[_config.env]


class AppConfig(Config):
    env = Choice(
        ["prod", "local", "staging"], 
        cast_function=str.lower,
        default="prod"
    )
    ...

storage = ChainMap(
  Env(),
  Yaml(get_config_file()),
)
config = AppConfig(storage)
```

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/ngladkikh/pkonfig",
    "name": "pkonfig",
    "maintainer": "Nikita Gladkikh",
    "docs_url": null,
    "requires_python": ">=3.6",
    "maintainer_email": "gladkikh.nikita@gmail.com",
    "keywords": "config,configuration,configurations,settings,env,environment,environments,dotenv,application,python-config,yaml,toml,ini",
    "author": "Nikita Gladkikh",
    "author_email": "gladkikh.nikita@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/bf/bb/c194301eb6d260daadd5ef3fed42520d195e74f0097c0dfe206fde8e4662/pkonfig-1.2.tar.gz",
    "platform": null,
    "description": "# PKonfig\n\n__P__ stands for __Python__.\n\n[![pypi](https://img.shields.io/pypi/v/pkonfig.svg)](https://pypi.python.org/pypi/pkonfig)\n[![downloads](https://img.shields.io/pypi/dm/pkonfig)](https://pepy.tech/project/pkonfig)\n[![versions](https://img.shields.io/pypi/pyversions/pkonfig.svg)](https://github.com/ngladkikh/pkonfig)\n[![license](https://img.shields.io/github/license/ngladkikh/pkonfig.svg)](https://github.com/ngladkikh/pkonfig/blob/master/LICENSE)\n\n## Prerequisites\n\n- Pythonic configuration management helpers.\n- Multiple sources of configs (environment variables, dotenv files, YAML, JSON, TOML, INI)\nwith agile order configuration.\n- Configs validation mechanics based on type hints or user defined classes.\n- Minimal external dependencies.\n- Follow [Fail-fast](https://en.wikipedia.org/wiki/Fail-fast) principle.\n- Autocomplete in modern IDEs.\n\n## Features\n\n- User defined config source order.\n- Multilevel configs for environment variables and dotenv config sources.\n- Custom aliases for fields or groups of configs.\n- Configs type casting\n- Config values validation based on type and/or value.\n- High performance.\n- Extendable API.\n\n## Installation\n\nTo install basic PKonfig without YAML and TOML support run:\n\n```bash\npip install pkonfig\n```\n\nYAML files parsing is handled with [PyYaml](https://pypi.org/project/PyYAML/):\n\n```bash\npip install pkonfig[yaml]\n```\n\nTOML files handled with help of [Tomli](https://pypi.org/project/tomli/):\n\n```bash\npip install pkonfig[toml]\n```\n\nAnd if both TOML and YAML is needed:\n\n```bash\npip install pkonfig[toml,yaml]\n```\n\nFor production no __.env__ files are needed but propper environment variables should be set.\nIn case some of required variables missing __KeyError__ exception raised while __AppConfig__\ninstantiation.\n\n## Quickstart\n\nThe most basic usage example when environment variables are used for production\nenvironment and DotEnv files are used for local development.\n\nCreate __config__ module __config.py__:\n\n```python\nfrom collections import ChainMap\nfrom pkonfig import Config, EmbeddedConfig, Env, DotEnv, LogLevel, Choice\n\n\nclass PG(EmbeddedConfig):\n  host = \"localhost\"\n  port = 5432\n  user: str\n  password: str\n\n  \nclass AppConfig(Config):\n    db = PG()\n    log_level = LogLevel(\"INFO\")\n    env = Choice([\"local\", \"prod\", \"test\"], default=\"prod\")\n\n\nstorage = ChainMap(DotEnv(\".env\", missing_ok=True), Env())\nconfig = AppConfig(storage)\n```\n\nFor local development create DotEnv file in root app folder __.env__:\n\n```dotenv\nAPP_DB_HOST=localhost\nAPP_DB_USER=postgres\nAPP_DB_PASSWORD=postgres\nAPP_ENV=local\nAPP_LOG_LEVEL=debug\n```\n\nThen elsewhere in app you could run:\n\n```python\nfrom config import config\n\nprint(config.env)           # local\nprint(config.log_level)     # 20\nprint(config.db.host)       # localhost\nprint(config.db.port)       # 5432\nprint(config.db.user)       # postgres\nprint(config.db.password)   # postgres\n```\n\n## Usage\n\n### Config sources\n\n__PKonfig__ implements several config sources out of the box.\nAll config sources implement `Mapping` protocol and default values could be set up during initialization.\n\n#### Environment variables\n\nThe most common way to configure application is environment variables.\nTo parse environment variables and store values in multilevel structure class `Env` could be used.\nCommon pattern is naming variables with multiple words describing the exact purpose \nmore precise: __PG_HOST__, __PG_PORT__ and __REDIS_HOST__, __REDIS_PORT__ could be treated as two groups:\n\n- PG\n  - HOST\n  - PORT\n- REDIS\n  - HOST\n  - PORT\n\nPKonfig respects this convention so that `Env` has two optional arguments:\n\n- `delimiter` string that will be used to split configuration levels taken from keys;\n- `prefix` string that is used to identify keys that are related to the given app and omit everything else.\n\n```python\nfrom os import environ\nfrom pkonfig import Env\n\n\nenviron[\"APP_OUTER\"] = \"foo\"\nenviron[\"APP_INNER_KEY\"] = \"baz\"\nenviron[\"NOPE\"] = \"qwe\"\n\nsource = Env(delimiter=\"_\", prefix=\"APP\", some_key=\"some\")\n\nprint(source[\"outer\"])          # foo\nprint(source[\"inner\"][\"key\"])   # baz\nprint(source[\"nope\"])           # raises KeyError\n```\n\n`Env` ignores key cases and ignores all keys starting not from __prefix__.\nTo change this behaviour set __prefix__ to `None`.\nIn this case you will get all key value pairs:\n\n```python\nfrom os import environ\nfrom pkonfig import Env\n\nenviron[\"NOPE\"] = \"qwe\"\n\nsource = Env(prefix=None)\n\nprint(source[\"nope\"])   # qwe\n```\n\n#### DotEnv\n\nIn the same manner as environment variables DotEnv files could be used.\n`DotEnv` requires file name as a string or a path and also accepts `delimiter` and `prefix` optional arguments.\n`missing_ok` argument defines whether `DotEnv` raises exception when given file not found.\nWhen file not found and `missing_ok` is set `DotEnv` contains empty dictionary.\n\n```python\nfrom pkonfig import DotEnv\n\n\nconfig_source = DotEnv(\"test.env\", delimiter=\"_\", prefix=\"APP\", missing_ok=True)\n```\n\n#### Ini\n\n__INI__ files are quite common and class `Ini` \nis build on top of [`configparser.ConfigParser`](https://docs.python.org/3/library/configparser.html):\n\n```python\nfrom pkonfig import Ini\n\nstorage = Ini(\"config.ini\", missing_ok=False)\nprint(storage[\"bitbucket.org\"][\"User\"])  # hg\nprint(storage[\"bitbucket.org\"][\"ServerAliveInterval\"])  # 45\n```\n\nIn case when __config.ini__:\n\n```ini\n[DEFAULT]\nServerAliveInterval = 45\n\n[bitbucket.org]\nUser = hg\n```\n\n`Ini` also accepts `missing_ok` argument to ignore missing file.\nMost of `ConfigParser` arguments are also accepted to modify parser behaviour.\n\n#### Json\n\n`Json` class uses `json.load` to read given JSON file and respects `missing_ok` argument:\n\n```python\nfrom pkonfig import Json\n\n\nstorage = Json(\"config.json\", missing_ok=False)\n```\n\n#### Yaml\n\nTo parse YAML files [PyYaml](https://pyyaml.org/wiki/PyYAMLDocumentation) could be used wrapped with `Yaml` class:\n\n```python\nfrom pkonfig import Yaml\n\nstorage = Yaml(\"config.yaml\", missing_ok=False)\n```\n\n#### Toml\n\nTOML files are parsed with [tomli](https://pypi.org/project/tomli/) wrapped with `Toml` helper class:\n\n```python\nfrom pkonfig import Toml\n\n\nstorage = Toml(\"config.toml\", missing_ok=False)\n```\n\n### Source order\n\nAny source for `BaseConfig` should implement `Mapper` protocol.\nSo it is easy to implement custom or combine existing implementations.\nRecommended way to combine multiple sources of configs is `ChainMap`:\n\n```python\nfrom collections import ChainMap\nfrom pkonfig import Env, DotEnv, Yaml\n\n\nconfig_source = ChainMap(\n    DotEnv(\"test.env\", missing_ok=True),\n    Env(),\n    Yaml(\"base_config.yaml\")\n)\n```\n\nIn this example we created `ChainMap` that looks for key until finds one in the given mappers sequence.\nThe first one source for configs is **test.env** file that might not exist and could be used for local development only.\nEnvironment variables are used as the second one config source.\nDotenv file will be preferred source in this example.\nThe last one source is **base_config.yaml** that should exist or `FileNotFoundError` exception raised.\n\nYou can customize source order in this way or even create your own logic implementing\n`Mapper` protocol.\n\n### Config\n\nTo implement application config class user should inherit from `pkonfig.config.Config` class and define\nrequired fields:\n\n```python\nfrom pkonfig import Config\n\n\nclass AppConfig(Config):\n    foo: float\n    baz: int\n\n\nstorage = {\"foo\": \"0.33\", \"baz\": 1}\nconfig = AppConfig(storage)\n\nprint(config.foo)   # 0.33\nprint(config.baz)   # 1\n```\n\nTo build more granular config structure `EmbeddedConfig` class is used:\n\n```python\nfrom pkonfig import Config, EmbeddedConfig\n\n\nclass Inner(EmbeddedConfig):\n    key: str\n\n\nclass AppConfig(Config):\n    inner = Inner()\n    foo: float\n    baz: int\n\n\nstorage = {\n    \"foo\": \"0.33\", \n    \"baz\": 1, \n    \"inner\": {\"key\": \"value\"}\n}\nconfig = AppConfig(storage)\n\nprint(config.inner.key)   # value\n```\n\n### Multilevel Config\n\nGrouping might be useful when there are lots of config parameters.\nTo achieve this `EmbeddedConfig` class should be inherited:\n\n```python\nfrom pkonfig import DotEnv, Config, EmbeddedConfig\n\n\nclass PgConfig(EmbeddedConfig):\n    host: str\n    port: int = 5432\n\n\nclass RedisConfig(EmbeddedConfig):\n    host: str\n    port: int = 6379\n\n\nclass AppConfig(Config):\n    pg = PgConfig()\n    redis = RedisConfig()\n\n\nconfig = AppConfig(\n    DotEnv(\".env\", delimiter=\"__\", prefix=\"APP\")\n)\n\nprint(config.pg.host)       # db_host\nprint(config.pg.port)       # 6432\nprint(config.redis.host)    # redis\n```\n\n__.env__ content:\n```dotenv\nAPP__PG__HOST=db_host\nAPP__PG__PORT=6432\nAPP__REDIS__HOST=redis\n```\nIn this example we customized delimiter with two underscores, default is '**_**'.\n\n### Aliases\n\nAll __Config__ fields accept __alias__ argument. \nWhen storage class searches for config attribute in its source either attribute\nname is used or alias when it is set.\n\n__config.py__:\n```python\nfrom pkonfig import DotEnv, EmbeddedConfig, Config, Int, Str\n\n\nclass HostConfig(EmbeddedConfig):\n    host: str\n    port: int\n    user: str\n    password = Str(alias=\"pass\")\n\n\nclass AppConfig(Config):\n    pg = HostConfig(alias=\"db\")\n    foo_baz = Int(alias=\"my_alias\")\n\n\nconfig = AppConfig(DotEnv(\".env\", delimiter=\"__\"))\n```\n\n__.env__ content:\n\n```dotenv\nAPP__DB__HOST=db_host\nAPP__DB__PORT=6432\nAPP__DB__PASS=password\nAPP__DB__USER=postgres\nAPP__MY_ALIAS=123\n```\n\nIn this example storage will seek in dotenv file parameters named by given alias.\nElsewhere in an app:\n\n```python\nfrom config import config\n\n\nprint(config.foo_baz)       # 123\nprint(config.pg.password)   # password\n```\n\n### PKonfig fields\n\nAll simple Python data types are implemented in field types: `Bool`, `Int`, `Float`, `Str`, `Byte`, `ByteArray`.\nAll fields with known type converted to descriptors during class creation.\nFields in `Config` classes may be defined in several ways:\n\n#### Using types:\n```python\nfrom pathlib import Path\nfrom pkonfig import Config\n\n\nclass AppConfig(Config):\n    foo: str\n    baz: int\n    flag: bool\n    file: Path\n```\n\n#### Using default values:\n\n```python\nfrom pathlib import Path\nfrom pkonfig import Config\n\n\nclass AppConfig(Config):\n    foo = \"some\"\n    baz = 1\n    flag = False\n    file = Path(\"some.text\")\n```\n\nGiven values will be used as default values.\n\n#### Using PKonfig fields directly\n\n```python\nfrom pkonfig import Config, PathField, Str, Int, Bool\n\n\nclass AppConfig(Config):\n    foo = Str()\n    baz = Int()\n    flag = Bool()\n    file = PathField()\n```\n\n#### Caching\n\nAll __PKonfig__ field types are Python descriptors that are responsible for type casting and data validation.\nIn most cases there is no need to do this job every time the value is accessed.\nTo avoid undesirable calculations caching is used.\nSo that type casting and validation is done only once \nduring `Config` object initialization.\nIn case when configuration may change during application lifecycle user may disable this behaviour:\n\n```python\nfrom pkonfig import Config, Int\n\n\nclass AppConfig(Config):\n    attr = Int(no_cache=True)\n```\n\nIn given example `attr` will do type casting and validation every time this attribute is accessed.\n\n#### Default values\n\nIf value is not set in config source user can use default value.\n`None` could be used as default value:\n\n```python\nfrom pkonfig import Config, Int, Str\n\n\nclass AppConfig(Config):\n    int_attr = Int(None)\n    str_attr = Str(None)\n\nconfig = AppConfig({})\nprint(config.str_attr)    # None\nprint(config.int_attr)    # None\n```\n\nWhen `None` is default value field is treated as nullable.\n\n#### Field nullability\n\nTo handle type casting and validation fields should not be nullable.\nIn case `None` is a valid value and should be used without casting and validation\noption `nullable` could be set:\n\n```python\nfrom pkonfig import Int, Config\n\n\nclass AppConfig(Config):\n    int_attr = Int(nullable=True)\n\nconfig = AppConfig(dict(int_attr=None))\nprint(config.int_attr)    # None\n```\n\nIn this example `None` comes from storage and type casting is omitted.\n\n### Custom descriptor or property\n\n```python\nfrom pkonfig import Config\n\n\nclass AppConfig(Config):\n    flag = True\n    baz = \"test\"\n    \n    @property\n    def value(self):\n        return self.flag and self.baz == \"test\" \n\n\nconfig = AppConfig({})\nprint(config.value)  # True\n```\n\n### Custom field types\n\nUser can customize how field validation and casting is done.\nThe recommended way is to implement `validate` method:\n\n```python\nfrom pkonfig import Config, Int\n\n\nclass OnlyPositive(Int):\n    def validate(self, value):\n        if value < 0:\n            raise ValueError(\"Only positive values accepted\")\n\n\nclass AppConfig(Config):\n    positive = OnlyPositive()\n```\n\nCustom type casting is also available.\nTo achieve this user should inherit abstract class `Field` and implement method `cast`:\n\n```python\nfrom typing import List\nfrom pkonfig import Field\n\nclass ListOfStrings(Field):\n    def cast(self, value: str) -> List[str]:\n        return value.split(\",\")\n```\n\n### Available fields\n\nBuiltin Python types has appropriate `Field` types:\n\n- bool -> `Bool`\n- int -> `Int`\n- float -> `Float`\n- Decimal -> `DecimalField`\n- str -> `Str`\n- bytes -> `Byte`\n- bytearray -> `ByteArray`\n\nThe only reason to use this types directly is customising field nullability and cache policy.\n\n#### PathField\n\nBasic path type that is parental for other two types and is used when you define field using `pathlib.Path`.\nThis type raises `FileNotFoundError` exception during initialization if given path doesn't exist:\n\n```python\nfrom pkonfig import Config, PathField\n\n\nclass AppConfig(Config):\n    mandatory_existing_path = PathField()\n    optional_path = PathField(missing_ok=True)\n```\n\nIn given example field `optional_path` may not exist during initialization.\n\n##### File\n\n`File` inherits `PathField` but also checks whether given path is a file.\n\n#### Folder\n\n`Folder` inherits `PathField` and does checking whether given path is a folder.\n\n#### EnumField\n\nThis field uses custom enum to validate input and cast it to given `Enum`:\n\n```python\nfrom enum import Enum\nfrom pkonfig import Config, EnumField\n\n\nclass UserType(Enum):\n    guest = 1\n    user = 2\n    admin = 3\n\n\nclass AppConfig(Config):\n    user_type = EnumField(UserType)\n\n\nconfig = AppConfig({\"user_type\": \"admin\"})\nprint(config.user_type is UserType.admin)  # True\n```\n\n#### LogLevel\n\n`LogLevel` field is useful to define `logging` level through configs.\n`LogLevel` accepts strings that define log level and casts \nthat string to `logging` level integer value:\n\n```python\nimport logging\nfrom pkonfig import Config, LogLevel\n\n\nclass AppConfig(Config):\n    some_level = LogLevel()\n    another_level = LogLevel()\n\n\nconfig = AppConfig(\n    {\n        \"some_level\": \"info\",\n        \"another_level\": \"Debug\",\n    }\n)\n\nprint(config.some_level)        # 20\nprint(config.another_level)     # 10\n\nprint(config.another_level is logging.DEBUG)     # True\n```\n\n#### Choice\n\n`Choice` field validates that config value is a member of the given sequence and also does optional type casting:\n\n```python\nfrom pkonfig import Config, Choice\n\n\nclass AppConfig(Config):\n    one_of_attr = Choice([10, 100], cast_function=int)\n\n\nconfig = AppConfig({\"one_of_attr\": \"10\"})\nprint(config.one_of_attr == 10)  # True\n\nconfig = AppConfig({\"one_of_attr\": \"2\"})    # raises TypeError exception\n```\n\nWhen `cast_function` is not given raw values from storage are used.\n\n#### DebugFlag\n\n`DebugFlag` helps to set widely used __debug__ option.\n`DebugFlag` ignores value case and treats __'true'__ string as `True` and any other value as `False`:\n\n```python\nfrom pkonfig import Config, DebugFlag\n\n\nclass AppConfig(Config):\n    lower_case = DebugFlag()\n    upper_case = DebugFlag()\n    random_string = DebugFlag()\n\n\nconfig = AppConfig(\n  {\n    \"lower_case\": \"true\",\n    \"upper_case\": \"TRUE\",\n    \"random_string\": \"foo\",\n  }\n)\nprint(config.lower_case)        # True\nprint(config.upper_case)        # True\nprint(config.random_string)     # False\n```\n\n### Types to Fields mapping\n\nAll fields for `BaseConfig` children classes are converted to descriptors internally.\nClass `pkonfig.config.DefaultMapper` defines how field types will be replaced with descriptors.\nThis mapping is used by default:\n```\n{\n    bool: Bool,\n    int: Int,\n    float: Float,\n    str: Str,\n    bytes: Byte,\n    bytearray: ByteArray,\n    Path: PathField,\n    Decimal: DecimalField,\n}\n```\n\nWhen field type is not found in this mapper it is ignored and won't be taken from storage source while resolving.\n\nUser can modify default mapper giving dictionary of types and appropriate fields:\n\n```python\nfrom decimal import Decimal\nfrom pkonfig import Config, DefaultMapper, DecimalField\n\n\nclass AppConfig(Config):\n    _mapper = DefaultMapper({float: DecimalField})\n    foo: float\n\nconfig = AppConfig(dict(foo=1/3))\nassert isinstance(config.foo, Decimal)  # True\n```\n\n### Per-environment config files\n\nWhen your app is configured with different configuration files \nand each file is used only in an appropriate environment you can create a function\nto find which file should be used:\n\n```python\nfrom pkonfig import Env, Yaml, Config, Choice\n\n\nCONFIG_FILES = {\n    \"prod\": \"configs/prod.yaml\",\n    \"staging\": \"configs/staging.yaml\",\n    \"local\": \"configs/local.yaml\",\n}\n\n\ndef get_config_file():\n    class _Config(Config):\n        env = Choice(\n          [\"prod\", \"local\", \"staging\"], \n          cast_function=str.lower,\n          default=\"prod\"\n        )\n    \n    _config = _Config(Env())\n    return CONFIG_FILES[_config.env]\n```\n\n__get_config_file__ uses environment variables and predefined config files pathes\nto check whether __APP_ENV__ var is set, validate this variable and return appropriate\nconfig file name.\nThen actual application configuration:\n\n```python\nfrom collections import ChainMap\nfrom pkonfig import Env, Yaml, Config, Choice\n\n\nCONFIG_FILES = {\n    \"prod\": \"configs/prod.yaml\",\n    \"staging\": \"configs/staging.yaml\",\n    \"local\": \"configs/local.yaml\",\n}\n\n\ndef get_config_file():\n    class _Config(Config):\n        env = Choice(\n          [\"prod\", \"local\", \"staging\"], \n          cast_function=str.lower,\n          default=\"prod\"\n        )\n    \n    _config = _Config(Env())\n    return CONFIG_FILES[_config.env]\n\n\nclass AppConfig(Config):\n    env = Choice(\n        [\"prod\", \"local\", \"staging\"], \n        cast_function=str.lower,\n        default=\"prod\"\n    )\n    ...\n\nstorage = ChainMap(\n  Env(),\n  Yaml(get_config_file()),\n)\nconfig = AppConfig(storage)\n```\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Pythonic agile application configuration helpers",
    "version": "1.2",
    "split_keywords": [
        "config",
        "configuration",
        "configurations",
        "settings",
        "env",
        "environment",
        "environments",
        "dotenv",
        "application",
        "python-config",
        "yaml",
        "toml",
        "ini"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "6bf144eb5bbde906efedcc4ba62c588653f892a9f52eddfd256918160f87bce6",
                "md5": "6123b6aef1247a25fea2f6cd2ee38bcc",
                "sha256": "7e6d3755c2cf1549f4a9130f078b9757fd2dafbf2b90020d8460e3844b761266"
            },
            "downloads": -1,
            "filename": "pkonfig-1.2-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "6123b6aef1247a25fea2f6cd2ee38bcc",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.6",
            "size": 14217,
            "upload_time": "2022-05-23T07:26:39",
            "upload_time_iso_8601": "2022-05-23T07:26:39.598005Z",
            "url": "https://files.pythonhosted.org/packages/6b/f1/44eb5bbde906efedcc4ba62c588653f892a9f52eddfd256918160f87bce6/pkonfig-1.2-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "bfbbc194301eb6d260daadd5ef3fed42520d195e74f0097c0dfe206fde8e4662",
                "md5": "a50166803ee289b92563df81de95b955",
                "sha256": "a0a11137cc65de46dc32d13758617f3e8d2779f7b34e969ae0819261fd88a806"
            },
            "downloads": -1,
            "filename": "pkonfig-1.2.tar.gz",
            "has_sig": false,
            "md5_digest": "a50166803ee289b92563df81de95b955",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.6",
            "size": 18605,
            "upload_time": "2022-05-23T07:26:41",
            "upload_time_iso_8601": "2022-05-23T07:26:41.756092Z",
            "url": "https://files.pythonhosted.org/packages/bf/bb/c194301eb6d260daadd5ef3fed42520d195e74f0097c0dfe206fde8e4662/pkonfig-1.2.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2022-05-23 07:26:41",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "github_user": "ngladkikh",
    "github_project": "pkonfig",
    "travis_ci": false,
    "coveralls": true,
    "github_actions": true,
    "lcname": "pkonfig"
}
        
Elapsed time: 0.04579s