ArFiSettings - flexible application settings management with Pydantic validation
arfi-settings
Installation
pip install -U arfi-settings
Advantages
- Contains all the functionality of pydantic-settings, but with more accurate name resolving.
- Inheritance is used when reading from any source.
- Specifying configuration sources is individual for each class of settings.
- Possibility to switch all settings by changing just one
MODEparameter. - Ability to switch between specific settings using the discriminator.
- Read configuration files with and without any extension.
- Configure reading command line parameters individually for the class and for the entire project.
- Clear configuration file structure out of the box with no need for pre-configuration. Flexible setting of absolutely all parameters.
- Easy creation of your own configuration read sources.
- Availability of connectors to the most common databases
- Specify your own default library settings in the
pyproject.tomlfile. - Debug Mode.
- And many other things ...
Features
Reverse inheritance
If a parent class, essentially the main settings class, has fields that inherit from ArFiSettings, then it will inherit settings from that parent class.
This behaviour can be switched off.
from arfi_settings import ArFiSettings, SettingsConfigDict
class SubChild(ArFiSettings):
pass
class Child(ArFiSettings):
sub_child: SubChild
class Parent(ArFiSettings):
child: Child
config = Parent()
print(config.conf_path)
#> [PosixPath('config/config')]
print(config.child.sub_child.conf_path)
#> [PosixPath('config/child/sub_child/config')]
print(config.settings_config.env_nested_delimiter)
#> ""
print(config.child.sub_child.settings_config.env_nested_delimiter)
#> ""
# Change settings in parent class
class Parent(ArFiSettings):
child: Child
model_config = SettingsConfigDict(
conf_dir=None,
conf_file=['appconfig.yaml', '~/.config/allacrity/config.toml'],
env_nested_delimiter="__",
)
config = Parent()
print(config.conf_path)
#> [PosixPath('appconfig.yaml'), PosixPath('/home/user/.config/allacrity/config.toml')]
print(config.child.sub_child.conf_path)
#> [PosixPath('child/sub_child/appconfig.yaml'), PosixPath('/home/user/.config/allacrity/config.toml')]
print(config.settings_config.env_nested_delimiter)
#> "__"
print(config.child.sub_child.settings_config.env_nested_delimiter)
#> "__"
model_config, file_config and env_config
Absolutely all settings can be specified via the model_config variable.
But the settings for files and environment variables can be set separately in file_config and env_config respectively.
The settings specified in file_config and env_config will take precedence and override the settings specified in model_config.
from arfi_settings import ArFiSettings, EnvConfigDict, FileConfigDict, SettingsConfigDict
class AppConfig(ArFiSettings):
file_config = FileConfigDict(
conf_case_sensitive=True,
)
env_config = EnvConfigDict(
env_case_sensitive=False,
)
model_config = SettingsConfigDict(
conf_case_sensitive=False,
env_case_sensitive=True,
)
config = AppConfig()
print(config.settings_config.conf_case_sensitive)
#> True
print(config.settings_config.env_case_sensitive)
#> False
Users Readers and Handlers
Adding your own readers and handlers.
from typing import Any
from arfi_settings import (
ArFiHandler,
ArFiReader,
ArFiSettings,
EnvConfigDict,
FileConfigDict,
SettingsConfigDict,
)
from arfi_settings.types import PathType
class AwessomReader(ArFiReader):
def my_custom_reader(self) -> dict[str, Any]:
data: dict[str, Any] = {}
# Do something ...
return data
class AwessomHandler(ArFiHandler):
reader_class = AwessomReader
def nonextension_ext_handler(self, file_path: PathType) -> dict[str, Any]:
reader = self.reader_class(
file_path=file_path,
file_encoding=self.config.conf_file_encoding,
ignore_missing=self.config.conf_ignore_missing,
)
data = reader.my_custom_reader()
# Do something ...
data["__case_sensitive"] = self.config.conf_case_sensitive
return data
# First way: Redefinition handler class inside main config class
class AppConfig(ArFiSettings):
handler_class = AwessomHandler
file_config = FileConfigDict(
conf_ext="json",
)
model_config = SettingsConfigDict(
conf_ext=["", "arfi"],
conf_custom_ext_handler={"": "nonextension", "arfi": "toml"},
)
config = AppConfig()
print(config.settings_config.conf_ext)
# > ['json']
# Second way: Redefinition Main handler class for all settings
ArFiSettings.handler_class = AwessomHandler
class AppConfig(ArFiSettings):
file_config = FileConfigDict(
conf_ext="json",
)
model_config = SettingsConfigDict(
conf_ext=["", "arfi"],
conf_custom_ext_handler={"": "nonextension", "arfi": "toml"},
)
config = AppConfig()
print(config.settings_config.conf_ext)
# > ['json']
# An alternative way: Redefinition Main handler inside class
class AwessomHandler(ArFiHandler):
def custom_main_handler(self) -> dict[str, Any]:
data: dict[str, Any] = {}
# Do something ...
return data
class AppConfig(ArFiSettings):
handler_class = AwessomHandler
handler = "custom_main_handler"
file_config = FileConfigDict(
conf_ext="json",
)
model_config = SettingsConfigDict(
conf_ext=["", "arfi"],
)
config = AppConfig()
print(config.settings_config.conf_ext)
# > ['json']
CLI
The CLI reader can be any callable object that returns dict[str, Any].
import argparse
from typing import Any
from arfi_settings import ArFiSettings, ArFiReader, SettingsConfigDict
def parse_args() -> dict[str, Any]:
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
argument_default=argparse.SUPPRESS,
)
parser.add_argument(
"--mode",
type=str,
help="Application mode",
)
cli_options = parser.parse_args()
data = dict(cli_options._get_kwargs())
return data
# Valid cli reader
ArFiReader.setup_cli_reader(parse_args)
# No Valid cli reader
# ArFiReader.setup_cli_reader(parse_args())
class AppConfig(ArFiSettings):
model_config = SettingsConfigDict(
cli=True,
)
config = AppConfig()
# if run python main.py --mode dev
print(config.model_dump_json())
#> {"MODE": "dev"}
class MyCliReader:
def __call__(self) -> dict[str, Any]:
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
argument_default=argparse.SUPPRESS,
)
parser.add_argument(
"--mode",
type=str,
help="Application mode",
)
cli_options = parser.parse_args()
data = dict(cli_options._get_kwargs())
return data
ArFiReader.setup_cli_reader(MyCliReader())
config = AppConfig()
# if run python main.py --mode dev
print(config.model_dump_json())
#> {"MODE": "dev"}
MODE
The main feature of this library is to change the mode of settings (MODE) during processing.
For example:
- create a directory with settings in a secret directory on the server.
- to specify all the settings files for a particular mode.
- specify this directory in the class
- specify in environment variables the mode of reading settings
MODE='prod'.
sudo mkdir -p /var/run/config
sudo touch /var/run/config/prod.toml
export MODE="prod"
# file /var/run/config/prod.toml
some_var = "test"
from arfi_settings import ArFiSettings, SettingsConfigDict
class AppConfig(ArFiSettings):
some_var: str
model_config = SettingsConfigDict(
conf_dir = ["config", "/var/run/config"],
)
config = AppConfig()
print(config.some_var)
#> test
Reading order of settings
By default:
ORDERED_SETTINGS = [
"cli",
"init_kwargs",
"env",
"env_file",
"secrets",
"conf_file",
]
To change:
from arfi_settings import ArFiSettings
class AppConfig(ArFiSettings):
# change for class
ordered_settings = ["conf_file", "env", "init_kwargs"]
# change for instance
config = AppConfig(_ordered_settings=["env", "conf_file"])
To create custom handler:
from arfi_settings import ArFiSettings, ArFiHandler
class MyHandler(ArFiHandler):
# method name must ends with `_ordered_settings_handler` and returns `dict[str, Any]`
def my_custom_ordered_settings_handler(self) -> dict[str, Any]:
data: dict[str, Any] = {}
# Do something ...
return data
class AppConfig(ArFiSettings):
handler_class = MyHandler
# Here you can specify the short name of the handler method
ordered_settings = ["my_custom", "init_kwargs"]
File pyproject.toml
In this file, set default values for each subclass of ArFiSettings.
The values set in the class override the values set in the file pyproject.toml.
[tool.arfi_settings]
env_config_inherit_parent = false
conf_dir = ["config", "/var/run/config"]
env_file_encoding = "cp1251"
arfi_debug = true # enable debug mode
The location of the pyproject.toml file is determined automatically. By default, the search is maximally 3 directories up.
If the file is undefined, you can set manually either the maximum search depth by the pyproject_toml_max_depth parameter, or the exact depth by the pyproject_toml_depth parameter. It is possible to prohibit the search and reading of settings from the pyproject.toml file individually for a class or for a class instance by setting the read_pyproject_toml=False parameter.
For example. We have a project structure:
~/my_project/
├── settings/
│ ├── __init__.py
│ └── settings.py
├── __init__.py
├── main.py
└── pyproject.toml
Best way
# file ~/my_project/settings/__init__.py
from arfi_settings import init_settings
init_settings.read_pyproject(read_once=True)
# For automatic search up to a maximum of 5 directories
# init_settings.read_pyproject(
# read_once=True,
# pyproject_toml_max_depth=5,
# )
# To specify the exact location of the `pyproject.toml` file
# init_settings.read_pyproject(
# read_once=True,
# pyproject_toml_depth=7,
# )
Alternative way
# file ~/my_project/settings/settings.py
from arfi_settings import ArFiSettings
class AppConfig(ArFiSettings):
pass
config = AppConfig(_pyproject_toml_max_depth=5)
# To disable reading settings from the `pyproject.toml` file
# config = AppConfig(_read_pyproject_toml=False)
For check path
# file ~/my_project/main.py
from settings.settings import config
print(config.pyproject_toml_path)
#> /home/user/my_project/pyproject.toml
A Simple Example
- Create
settings.py
from typing import Literal
from pydantic import Field
from arfi_settings import ArFiSettings, SettingsConfigDict, EnvConfigDict
class Database(ArFiSettings):
DIALECT: Literal["sqlite", "mysql", "postgres"]
DATABASE: str
# Create common nested directory for read settings from config/db for sqlite, mysql, postgres
mode_dir = "db"
# Disable inheritance of settings from parent
env_config_inherit_parent = False
# Create env prefix as sqlite_, mysql_, postgres_
env_config = EnvConfigDict(env_prefix_as_source_mode_dir=True)
class SQLite(Database):
mode_dir = "sqlite"
DIALECT: Literal["sqlite"] = "sqlite"
DATABASE: str = "default_database"
class MySQL(Database):
mode_dir = "mysql"
DIALECT: Literal["mysql"] = "mysql"
DATABASE: str
class PostgreSQL(Database):
mode_dir = "postgres"
DIALECT: Literal["postgres"] = "postgres"
DATABASE: str
class AppConfig(ArFiSettings):
db: SQLite | MySQL | PostgreSQL = Field(SQLite(), discriminator="DIALECT")
# set env delimiter
model_config = SettingsConfigDict(env_nested_delimiter="__")
config = AppConfig()
print(config.db.DIALECT)
# > sqlite
print(config.db.DATABASE)
# > default_database
- Create file
config/config.toml
[db]
Database = "main_config_database"
Result:
config = AppConfig()
print(config.db.DIALECT)
# > sqlite
print(config.db.DATABASE)
# > main_config_database
- Create file
.env
DB__DATABASE = "env_database"
Result:
config = AppConfig()
print(config.db.DIALECT)
# > sqlite
print(config.db.DATABASE)
# > env_database
- Create file
config/db/sqlite/config.toml
database = "sqlite_config_database"
Result:
config = AppConfig()
print(config.db.DIALECT)
# > sqlite
print(config.db.DATABASE)
# > sqlite_config_database
- Modify file
.env
DB__DATABASE = "env_database"
SQLITE_DATABASE = "sqlite_env_database"
Result:
config = AppConfig()
print(config.db.DIALECT)
# > sqlite
print(config.db.DATABASE)
# > sqlite_env_database
- Create file
config/db/postgres/config.toml
database = 'postgres_database_config'
Modify file config/config.toml
[db]
Database = "main_config_database"
dialect = "postgres"
Result:
config = AppConfig()
print(config.db.DIALECT)
# > postgres
print(config.db.DATABASE)
# > postgres_database_config
- Create file
config/db/postgres/prod.toml
database = 'postgres_database_config_prod'
Modify file .env
DB__DATABASE = "env_database"
SQLITE_DATABASE = "sqlite_env_database"
DB__MODE = "prod"
Result:
config = AppConfig()
print(config.db.DIALECT)
# > postgres
print(config.db.DATABASE)
# > postgres_database_config_prod
- Modify file
.env
DB__DATABASE = "env_database"
SQLITE_DATABASE = "sqlite_env_database"
DB__MODE = "test"
DB__DIALECT = "mysql"
Create file config/db/mysql/test.yaml
database: "mysql_database_config_test"
Result:
config = AppConfig()
print(config.db.DIALECT)
# > mysql
print(config.db.DATABASE)
# > mysql_database_config_test
- Modify file
.env
DB__DATABASE = "env_database"
SQLITE_DATABASE = "sqlite_env_database"
DB__MODE = "test"
DB__DIALECT = "mysql"
MYSQL_DATABASE = "mysql_database_env"
Result:
config = AppConfig()
print(config.db.DIALECT)
# > mysql
print(config.db.DATABASE)
# > mysql_database_env
- Set environment variables
export DB__DIALECT="postgres"
export POSTGRES_DATABASE="postgres_database_from_enviroment"
Result:
config = AppConfig()
print(config.db.DIALECT)
# > postgres
print(config.db.DATABASE)
# > postgres_database_from_enviroment
Roadmap
- Create documentation
- Read settings from files without creating a model, but with the ability to use
MODEas for the main settings class - Reading settings from
URL - Reading encrypted settings with key specification
- Extend the usability for python versions
3.8,3.9,3.10 - Expand debugging mode.
Logo
Logo courtesy of Alex Zalevski
Metadata
Release files for arfi-settings 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| arfi_settings-0.4.0.tar.gz | 63.5 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| arfi_settings-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 108.3 kB
Release files / arfi_settings-0.4.0.tar.gz
| Download URL | arfi_settings-0.4.0.tar.gz |
|---|---|
| Size | 63.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
27c31dd6bf6243aa2ac9605bdc8d450ab2541be6d22882da0c1c54bfaf8b1b2e
|
|
BLAKE2b-256 checksum How to use checksums |
ebd4e1ab6358fe9797793ad0e6a97f6cb725b542efb94fbd14bd423af96305ea
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
pdm/2.17.1 CPython/3.8.10 Linux/5.4.0-190-generic
|
Release files / arfi_settings-0.4.0-py3-none-any.whl
| Download URL | arfi_settings-0.4.0-py3-none-any.whl |
|---|---|
| Size | 44.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
766cc9fa7a08429df9c64f1e8894c179c6f725f1fa9b988154b280c244e93323
|
|
BLAKE2b-256 checksum How to use checksums |
ff02e3aa8e8144f345bb09cbe68b7e79e4fdc0153329c3338a9bc98a0f12cc26
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
pdm/2.17.1 CPython/3.8.10 Linux/5.4.0-190-generic
|
Security for arfi-settings 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.