Skip to content

Commit

Permalink
Upgrade deps for Python 3.11 🐍
Browse files Browse the repository at this point in the history
  • Loading branch information
RobertoPrevato authored Nov 5, 2022
1 parent 734b984 commit 9c583dd
Show file tree
Hide file tree
Showing 14 changed files with 207 additions and 84 deletions.
16 changes: 8 additions & 8 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ env:

jobs:
build:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.6, 3.7, 3.8, 3.9]
python-version: [3.8, 3.9, "3.10", "3.11"]

steps:
- uses: actions/checkout@v1
Expand All @@ -28,7 +28,7 @@ jobs:
submodules: false

- name: Use Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

Expand Down Expand Up @@ -72,28 +72,28 @@ jobs:
- name: Install distribution dependencies
run: pip install --upgrade twine setuptools wheel
if: matrix.python-version == 3.9
if: matrix.python-version == 3.10

- name: Create distribution package
run: python setup.py sdist bdist_wheel
if: matrix.python-version == 3.9
if: matrix.python-version == 3.10

- name: Upload distribution package
uses: actions/upload-artifact@master
with:
name: dist-package-${{ matrix.python-version }}
path: dist
if: matrix.python-version == 3.9
if: matrix.python-version == 3.10

publish:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'release'
steps:
- name: Download a distribution artifact
uses: actions/download-artifact@v2
with:
name: dist-package-3.9
name: dist-package-3.10
path: dist
- name: Publish distribution 📦 to Test PyPI
uses: pypa/gh-action-pypi-publish@master
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0] - 2022-11-04 :snake:
- Upgrades pinned dependencies for Python 3.11
- Adds the alias "EnvVars" to reduce the verbosity of the class name "EnvironmentVariables"
- Adds support for TOML sources
- Replaces relative imports with absolute imports
- Workflow maintenance

## [0.0.2] - 2021-08-11 :cactus:
- Forks a new project from
[roconfiguration](https://github.com/Neoteroi/roconfiguration), with name
Expand Down
96 changes: 68 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,25 @@ Implementation of key-value pair based configuration for Python applications.
* support for nested structures and lists, using attribute notation
* strategy to use environment specific settings

This library is freely inspired by .NET Core `Microsoft.Extensions.Configuration` namespace and its pleasant design (_ref. [MSDN documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-2.1), [Microsoft Extensions Configuration Deep Dive](https://www.paraesthesia.com/archive/2018/06/20/microsoft-extensions-configuration-deep-dive/)_).
This library is freely inspired by .NET Core `Microsoft.Extensions.Configuration` (_ref. [MSDN documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-2.1), [Microsoft Extensions Configuration Deep Dive](https://www.paraesthesia.com/archive/2018/06/20/microsoft-extensions-configuration-deep-dive/)_).

The main class is influenced by Luciano Ramalho`s example of
JSON structure explorer using attribute notation, in his book [Fluent Python](http://shop.oreilly.com/product/0636920032519.do).

## Overview

`essentials-configuration` provides a way to handle configuration roots
composed of different layers, such as configuration files and environmental
composed of different layers, such as configuration files and environment
variables. Layers are applied in order and can override each others' values,
enabling different scenarios like configuration by environment and system
instance.

## Supported sources:
* **toml** files
* **yaml** files
* **json** files
* **ini** files
* environmental variables
* environment variables
* dictionaries
* keys and values
* [Azure Key Vault](https://docs.microsoft.com/en-us/azure/key-vault/general/basic-concepts), using [essentials-configuration-keyvault](https://github.com/Neoteroi/essentials-configuration-keyvault)
Expand All @@ -56,21 +57,56 @@ pip install essentials-configuration[yaml]

# Examples

### JSON file and environmental variables
### TOML file

In this example, configuration values will include the structure inside the
file `settings.json` and environmental variables whose name starts with "APP_".
Settings are applied in order, so environmental variables with matching name
override values from the `json` file.
```python
from configuration.common import ConfigurationBuilder
from configuration.toml import TOMLFile
from configuration.env import EnvVars

builder = ConfigurationBuilder(
TOMLFile("settings.toml"),
EnvVars(prefix="APP_")
)

config = builder.build()
```

For example, if the TOML file contains the following contents:

```json
title = "TOML Example"

[owner]
name = "Tom Preston-Werner"
```

And the environment has a variable named `APP_OWNER__NAME=AAA`:

```python
>>> config
<Configuration {'title': '...', 'owner': '...'}>
>>> config.title
'TOML Example'
>>> config.owner.name
'AAA'
```

### JSON file and environment variables

In the following example, configuration values will include the structure
inside the file `settings.json` and environment variables whose name starts
with "APP_". Settings are applied in order, so environment variables with
matching name override values from the `json` file.

```python
from configuration.common import ConfigurationBuilder
from configuration.json import JSONFile
from configuration.env import EnvironmentVariables
from configuration.env import EnvVars

builder = ConfigurationBuilder(
JSONFile("settings.json"),
EnvironmentVariables(prefix="APP_")
EnvVars(prefix="APP_")
)

config = builder.build()
Expand Down Expand Up @@ -99,36 +135,38 @@ And the environment has a variable named `APP_foo=AAA`:
'INFO'
```

### YAML file and environmental variables
### YAML file and environment variables

In this example, configuration will include anything inside a file
`settings.yaml` and environmental variables. Settings are applied in order, so
environmental variables with matching name override values from the `yaml` file
`settings.yaml` and environment variables. Settings are applied in order, so
environment variables with matching name override values from the `yaml` file
(using the `yaml` source requires also `PyYAML` package).


```python
from configuration.common import ConfigurationBuilder
from configuration.env import EnvironmentVariables
from configuration.env import EnvVars
from configuration.yaml import YAMLFile

builder = ConfigurationBuilder()

builder.add_source(YAMLFile("settings.yaml"))
builder.add_source(EnvironmentVariables())
builder.add_source(EnvVars())

config = builder.build()
```

### YAML file, optional file by environment
In this example, if an environmental variable with name `APP_ENVIRONMENT` and

In this example, if an environment variable with name `APP_ENVIRONMENT` and
value `dev` exists, and a configuration file with name `settings.dev.yaml` is
present, it is read to override values configured in `settings.yaml` file.

```python
import os

from configuration.common import ConfigurationBuilder
from configuration.env import EnvironmentVariables
from configuration.env import EnvVars
from configuration.yaml import YAMLFile

environment_name = os.environ["APP_ENVIRONMENT"]
Expand All @@ -139,22 +177,22 @@ builder.add_source(YAMLFile("settings.yaml"))

builder.add_source(YAMLFile(f"settings.{environment_name}.yaml", optional=True))

builder.add_source(EnvironmentVariables(prefix="APP_"))
builder.add_source(EnvVars(prefix="APP_"))

config = builder.build()
```

### Filtering environmental variables by prefix
### Filtering environment variables by prefix

```python
from configuration.common import Configuration
from configuration.common import ConfigurationBuilder
from configuration.env import EnvVars

builder = ConfigurationBuilder()

config = Configuration()
builder.add_source(EnvVars(prefix="APP_"))

# will read only environmental variables
# starting with "APP_", case insensitively, removing the "APP_" prefix by
# default
config.add_environmental_variables("APP_")
config = builder.build()
```

### INI files
Expand Down Expand Up @@ -212,7 +250,7 @@ assert config.port == 44555

### Overriding nested values

It is possible to override nested values by environmental variables or
It is possible to override nested values by environment variables or
dictionary keys using the following notation for sub properties:

* keys separated by colon ":", such as `a:d:e`
Expand Down Expand Up @@ -253,13 +291,14 @@ assert config.a.d.f == 4
```

### Overriding nested values using env variables

```python
import os

builder = ConfigurationBuilder(
MapSource(
{
"a": {
"a": {Env
"b": 1,
"c": 2,
"d": {
Expand All @@ -286,7 +325,7 @@ assert config.a.d.f == 4

os.environ["a__d__e"] = "5"

builder.sources.append(EnvironmentVariables())
builder.sources.append(EnvVars())

config = builder.build()

Expand Down Expand Up @@ -319,6 +358,7 @@ assert config.b2c[2].tenant == "3"
```

### Goal and non-goals

The goal of this package is to provide a way to handle configuration roots,
fetching and composing settings from different sources, usually happening
once at application's start.
Expand Down
2 changes: 1 addition & 1 deletion configuration/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from collections import abc
from typing import Any, Dict, List, Mapping, Optional

from ..errors import ConfigurationOverrideError
from configuration.errors import ConfigurationOverrideError


def apply_key_value(obj, key, value):
Expand Down
5 changes: 4 additions & 1 deletion configuration/env/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
from typing import Any, Dict, Optional

from ..common import ConfigurationSource
from configuration.common import ConfigurationSource


class EnvironmentVariables(ConfigurationSource):
Expand All @@ -24,3 +24,6 @@ def get_values(self) -> Dict[str, Any]:
key_lower = key_lower[len(prefix) :]
values[key_lower] = value
return values


EnvVars = EnvironmentVariables
4 changes: 2 additions & 2 deletions configuration/ini/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from collections import abc
from typing import Any, Dict

from ..common import ConfigurationSource
from ..errors import MissingConfigurationFileError
from configuration.common import ConfigurationSource
from configuration.errors import MissingConfigurationFileError


def _develop_configparser_values(parser):
Expand Down
8 changes: 4 additions & 4 deletions configuration/json/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import os
from typing import Any, Dict

from ..common import ConfigurationSource
from ..errors import MissingConfigurationFileError
from configuration.common import ConfigurationSource
from configuration.errors import MissingConfigurationFileError


class JSONFile(ConfigurationSource):
Expand All @@ -21,5 +21,5 @@ def get_values(self) -> Dict[str, Any]:
return {}
raise MissingConfigurationFileError(self.file_path)

with open(self.file_path, "rt", encoding="utf-8") as source_file:
return json.load(source_file)
with open(self.file_path, "rt", encoding="utf-8") as source:
return json.load(source)
34 changes: 34 additions & 0 deletions configuration/toml/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import os
from typing import Any, Callable, Dict

try:
# Python 3.11
import tomllib
except ImportError: # pragma: no cover
# older Python
import tomli as tomllib # noqa

from configuration.common import ConfigurationSource
from configuration.errors import MissingConfigurationFileError


class TOMLFile(ConfigurationSource):
def __init__(
self,
file_path: str,
optional: bool = False,
parse_float: Callable[[str], Any] = float,
) -> None:
super().__init__()
self.file_path = file_path
self.optional = optional
self.parse_float = parse_float

def get_values(self) -> Dict[str, Any]:
if not os.path.exists(self.file_path):
if self.optional:
return {}
raise MissingConfigurationFileError(self.file_path)

with open(self.file_path, "rb") as source:
return tomllib.load(source, parse_float=self.parse_float)
Empty file added configuration/toml/py.typed
Empty file.
13 changes: 5 additions & 8 deletions configuration/yaml/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

import yaml

from ..common import ConfigurationSource
from ..errors import MissingConfigurationFileError
from configuration.common import ConfigurationSource
from configuration.errors import MissingConfigurationFileError


class YAMLFile(ConfigurationSource):
Expand All @@ -22,10 +22,7 @@ def get_values(self) -> Dict[str, Any]:
return {}
raise MissingConfigurationFileError(self.file_path)

with open(self.file_path, "rt", encoding="utf-8") as f:
with open(self.file_path, "rt", encoding="utf-8") as source:
if self.safe_load:
data = yaml.safe_load(f)
else:
data = yaml.full_load(f)

return data
return yaml.safe_load(source)
return yaml.full_load(source)
Loading

0 comments on commit 9c583dd

Please sign in to comment.