JSON Configuration File Framework
Project description
json-settings
json-settings is a Python framework for JSON configuration file handling. It provides the following features
- Define a nested Python class structure that mirrors the desired configuration file.
- Automatic type checking.
- Implicit recursive error messaging that provides human readable information on the location and nature of an error in a configuration file.
- Easy and adaptable value bounding validation.
- Array and range support for numerical values.
- Ability to convert a setting object with range values into a multidimensional array of the settings object with singular values for each setting.
Contents
Installation
Install the json-settings package with the command pip install json_settings
.
Getting Started
Primitive-Types
We would like to create a simple configuration file, my_json_config_file.json, with a single setting that is an integer. The JSON file will look like this:
{
"my_integer": 1
}
So we use the basic unit of json_settings, the Settings
base class, to define
a new Settings
derived class
import json
from json_settings import Settings
class MyCoolSetting(Settings):
@Settings.assign
def __init__(self, value: dict):
self.my_integer = int
if __name__ == "__main__":
with open("my_json_config_file.json", 'r') as f:
values = json.loads(f.read())
my_cool_setting = MyCoolSetting(values)
print(my_cool_setting.my_integer)
print(type(my_cool_setting.my_integer))
If we run the Python above the output will be
1
<class `int`>
A few things to note:
- All user defined settings classes must call their base class'
assign
decorator on the__init__
method. - All user define settings class'
__init__
method take a single argument (in addition toself
). - All settings defined in the
__init__
method must be equal to their required type. - Any variables defined in the
__init__
method will be enforced at runtime. - JSON entries cannot contain hyphens in their id string.
Reference-Types
We now want to have a settings file that is more complex. The configuration file will look like this:
{
"footware": {
"type": "formal",
"quantity": 2
},
"gloves": true
}
The corresponding Python class structure is as follows:
import json
from json_settings import Settings
class FootwareSettings(Settings):
@Settings.assign
def __init__(self, values):
self.type = str
self.quantity = int
class MyCoolSetting(Settings):
@Settings.assign
def __init__(self, value: dict):
self.footware = FootwareSettings
self.gloves = bool
if __name__ == "__main__":
with open("my_json_config_file.json", 'r') as f:
values = json.loads(f.read())
my_cool_setting = MyCoolSetting(values)
print(my_cool_setting.footware.type)
print(my_cool_setting.footware.quantity)
print(my_cool_setting.gloves)
If we run the Python above the output will be
formal
2
True
This nesting can be of an arbitrary depth, and all error handling is automatic and recursive, allowing for easy construction of complex configuration files.
Null-Types
The standard json package converts null
values to None
type values. By
default Settings
derived classes will assign None
regardless of the required
type. This can be restricted by using a TerminusSetting
derived type.
Terminus-Setting
Sometimes we need to define a setting which has more rigorous constraints. To do
this we define a TerminusSetting
derived class.
We want to define a setting that is the name of a king, however we require that it starts with "king_".
{
"my_king": "king_james"
}
The corresponding Python class structure is as follows:
import json
from json_settings import TerminusSetting
from json_settings import Settings
class MyCoolSetting(Settings):
@Settings.assign
def __init__(self):
self.my_king = KinglyName
class KinglyName(TerminusSetting):
@TerminusSetting.assign
def __init__(self, value: dict):
self.type = str
def check(self):
if not self.value.startswith("king_"):
raise ValueError('Name must start with "king_"')
if __name__ == "__main__":
with open("my_json_config_file.json", 'r') as f:
values = json.loads(f.read())
my_kingly_setting = MyCoolSetting(values)
print(my_kingly_setting.my_king)
If we run the Python above the output will be
king_james
A few things to note:
- TerminusSetting derived classes must define only one attribute
type
in the__init__
method, which is the type of the variable stored. - The abstract
check
method must be defined for all TerminusSetting derived classes. - The check method will catch
ValueError
andTypeError
and raiseSettingCheckError
, which in turn is caught by the enclosingSettings
derived instance and raised as aSettingErrorMessage
. - The value of the setting in a
TerminusSetting
derived class is stored in thevalue
attribute. - When an attribute of a
Settings
object which is aTerminusSetting
derived type is accessed, the value stored in theTerminusSetting
derived instances is returned, NOT theTerminusSettings
derived instance itself.
Setting-Error-Messages
One of the key features of json_settings is the recursive exception handling. To demonstrate we define the following configuration file and corresponding Python class structure
{
"first": {
"second": {
"fourth": 1,
"fith": "fith"
},
"third": false
}
}
import sys
import json
from json_settings import Settings
from json_settings import TerminusSetting
from json_settings import SettingErrorMessage
class MainSettings(Settings):
@Settings.assign
def __init__(self, values):
self.first = SecondSettings
class SecondSettings(Settings):
@Settings.assign
def __init__(self, values):
self.second = FinalSettings
self.third = bool
class FinalSettings(Settings):
@Settings.assign
def __init__(self, values):
self.fourth = int
self.fith = FithSetting
class FithSetting(TerminusSetting):
@TerminusSetting.assign
def __init__(self, value):
self.type = str
def check(self):
if "f" not in self.value:
raise ValueError('Must contain the letter "f"')
if __name__ == "__main__":
with open("my_json_config_file.json", 'r') as f:
values = json.loads(f.read())
try:
my_cool_settings= MainSettings(values)
except SettingErrorMessage as e:
sys.exit(e)
Instead of running the above code with the correct JSON configuration file, we will use the following, which contains an error
{
"first": {
"second": {
"fourth": 1,
"fith": "badger"
},
"third": false
}
}
Running the above code will yield the following output
first -> second -> fith -> Must contain the letter "f"
If we have an error in a different setting like so
{
"first": {
"second": {
"fourth": 1,
"fith": "fith"
},
"third": 1
}
}
we get the following error message
first -> third -> Expecting : <class 'bool'> | Received: <class 'int'>
In each case the location and nature of the error in the configuration file is indicated in the error message yielded to the user.
Number-Settings
json_settings provides a special base class for numerical settings.
NumberSettings
is itself derived from TerminusSetting
, but with some extra
functionality.
Imagine we wish to create a settings object with a float
setting, that must be
greater than or equal to zero:
{
"important_number": 1.0
}
import sys
import json
from json_settings import Settings
from json_settings import NumberSetting
from json_settings import SettingErrorMessage
class MainSettings(Settings):
@Settings.assign
def __init__(self, values):
self.important_number = ImportantNumberSetting
class ImportantNumberSetting(NumberSetting):
@NumberSetting.assign
def __init__(self, value):
self.type = float
def check(self):
self.lower_bound(0.0)
if __name__ == "__main__":
with open("my_json_config_file.json", 'r') as f:
values = json.loads(f.read())
try:
my_cool_settings = MainSettings(values)
except SettingErrorMessage as e:
sys.exit(e)
print(f"Important Number is: {my_cool_settings.important_number})
Notes:
NumberSetting
comes with several inbuild check methods for enforcing numberical boundslower_bound
upper_bound
lower_bound_exclusive
upper_bound_exclusive
Running the above code will return
Important Number is: 1.0
However NumberSetting
derived settings objects also accept array and range
definitions. For example, if we use the following configuration file
{
"important_number": {
"array": [1.0, 2.0, 3.0]
}
}
we get the following output
Important Number is: [1.0, 2.0, 3.0]
We also note that if one of entries in the array does not satisfy the range condition we get the following output
important_number -> must be >= 0.
We can also define a set of values in the following way
{
"important_number": {
"min": 0.0,
"max": 5.0,
"num": 6
}
}
which gives the following output
Important Number is: [0.0, 1.0, 2.0, 3.0, 4.0, 5.0]
where we can see a linear space has been created over the defined range.
Errors in the range definition are caught and yielded to the user such as
important_number -> No 'min' parameter provided for range
Note:
- The order of the range or array does not matter.
Spaces
Consider a situation where we have some NumberSetting
settings in our
configuration file
{
"primary_number": {
"array": [1.0, 2.0, 3.0]
},
"secondary_number": {
"min": 4.0,
"max": 6.0,
"num": 3
}
}
import sys
import json
from json_settings import Settings
from json_settings import NumberSetting
from json_settings import SettingErrorMessage
class MainSettings(Settings):
@Settings.assign
def __init__(self, values):
self.primary_number = ImportantNumberSetting
self.secondary_number = ImportantNumberSetting
class ImportantNumberSetting(NumberSetting):
@NumberSetting.assign
def __init__(self, value):
self.type = float
def check(self):
self.lower_bound(0.0)
if __name__ == "__main__":
with open("my_json_config_file.json", 'r') as f:
values = json.loads(f.read())
try:
my_cool_settings = MainSettings(values)
except SettingErrorMessage as e:
sys.exit(e)
print(f"Primary Number: {my_cool_settings.primary_number}")
print(f"Secondary Number: {my_cool_settings.secondary_number}")
Running the above will output the following, indicating that the two arrays are stored
Primary Number: [1.0, 2.0, 3.0]
Secondary Number: [4.0, 5.0, 6.0]
However what if we want to generate a set of MainSettings
instances, each one
representing a single point in combined cartesian product space of
primary_number
and secondary_number
. We can do so using the Space
class.
import sys
import json
from json_settings import Settings
from json_settings import NumberSetting
from json_settings import SettingErrorMessage
from json_settings import Space
class MainSettings(Settings):
@Settings.assign
def __init__(self, values):
self.primary_number = ImportantNumberSetting
self.secondary_number = ImportantNumberSetting
class ImportantNumberSetting(NumberSetting):
@NumberSetting.assign
def __init__(self, value):
self.type = float
def check(self):
self.lower_bound(0.0)
if __name__ == "__main__":
with open("my_json_config_file.json", 'r') as f:
values = json.loads(f.read())
try:
my_cool_settings = MainSettings(values)
except SettingErrorMessage as e:
sys.exit(e)
settings_space = Space(my_cool_settings)
print(f"Primary Number[0, 0]: {settings_space[0, 0].primary_number}")
print(f"Secondary Number[0, 0]: {settings_space[0, 0].secondary_number}")
print(f"Type of [0, 0] element: {settings_space[0, 0]}")
print(f"Primary Number[0, 1]: {settings_space[0, 1].primary_number}")
print(f"Secondary Number[0, 1]: {settings_space[0, 1].secondary_number}")
print(f"Primary Number[2, 2]: {settings_space[2, 2].primary_number}")
print(f"Secondary Number[2, 2]: {settings_space[2, 2].secondary_number}")
print(f"Space dimensions: {settings_space.shape}")
print(f"Space summary: {settings_space.cout_summary()}")
print(f"Total number of elements: {len(settings_space)}")
The above will output
Primary Number[0, 0]: 1.0
Secondary Number[0, 0]: 4.0
Type of [0, 0] element: <__main__.MainSettings object at 0x000001BF79B40F10>
Primary Number[0, 1]: 1.0
Secondary Number[0, 1]: 5.0
Primary Number[2, 2]: 3.0
Secondary Number[2, 2]: 6.0
Space dimensions: (3, 3)
Space summary: Computational space dimensions: 3 x 3
axis: 0:
primary_number
values: [1.0, 2.0, 3.0]
axis: 1:
secondary_number
values: [4.0, 5.0, 6.0]
Total number of elements: 9
The Space
instances behaves as a multidimensional numpy
array.
A few things to note:
- You can define an arbitrary number of ranged parameters. The resultant
Space
instance will have the corresponding number of dimensions. - You can restrict the space range expansion to a particular subset of the
available settings by passing the optional
restrict
parameter to theSpace
constructor- restrict :
dict
[str
,str
] A dictionary ofstr
:str
pairs that are used to exclude subsetting branches from the exploration function for finding ranges. If when searching the settings object for ranges, a setting with the same name as a key inrestrict
is found, only subsettings with name equal to the corresponding value will be searched.
- restrict :
Range-Matching
Sometimes we might have several ranges, however we want to couple some of them
together such that the resulting Space
instance is of reduced dimension.
{
"primary_number": {
"array": [1.0, 2.0, 3.0]
},
"secondary_number": {
"min": 4.0,
"max": 6.0,
"num": 3
},
"tertiary_number": {
"array": [-1.0, -2.0, -3.0]
}
}
import sys
import json
from json_settings import Settings
from json_settings import NumberSetting
from json_settings import SettingErrorMessage
from json_settings import Space
class MainSettings(Settings):
@Settings.assign
def __init__(self, values):
self.primary_number = ImportantNumberSetting
self.secondary_number = ImportantNumberSetting
self.tertiary_number = MinorNumberSetting
class ImportantNumberSetting(NumberSetting):
@NumberSetting.assign
def __init__(self, value):
self.type = float
def check(self):
self.lower_bound(0.0)
class MinorNumberSetting(NumberSetting):
@NumberSetting.assign
def __init__(self, value):
self.type = float
def check(self):
pass
if __name__ == "__main__":
with open("my_json_config_file.json", 'r') as f:
values = json.loads(f.read())
try:
my_cool_settings = MainSettings(values)
except SettingErrorMessage as e:
sys.exit(e)
settings_space = Space(my_cool_settings)
print(f"Space dimensions: {settings_space.shape}")
print(f"Space summary: {settings_space.cout_summary()}")
print(f"Total number of elements: {len(settings_space)}")
This will result in the following output
Space dimensions: (3, 3, 3)
Space summary: Computational space dimensions: 3 x 3 x 3
axis: 0:
primary_number
values: [1.0, 2.0, 3.0]
axis: 1:
secondary_number
values: [4.0, 5.0, 6.0]
axis: 2:
tertiary_number
values: [-1.0, -2.0, -3.0]
Total number of elements: 27
We can couple two of the ranges together, such that the resultant space iterates through matched parameters in step. Using the following JSON file
{
"primary_number": {
"array": [1.0, 2.0, 3.0],
"match": "best_match"
},
"secondary_number": {
"min": 4.0,
"max": 6.0,
"num": 3,
},
"tertiary_number": {
"array": [-1.0, -2.0, -3.0],
"match": "best_match"
}
}
the resultant output is
Space dimensions: (3, 3)
Space summary: Computational space dimensions: 3 x 3
axis: 0:
secondary_number
values: [4.0, 5.0, 6.0]
axis: 1:
match_id: best_match
primary_number
tertiary_number
values: [(1.0, -1.0), (2.0, -2.0), (3.0, -3.0)]
Total number of elements: 9
We can see that primary_number
and `tertiary_number are coupled in order along
one axis.
A few things to note:
- You can match an arbitary number of ranges together at different and arbitrary depth.
- You can use any match string, and two ranges with the same match string will be coupled.
- You can define an arbitrary number of match strings.
String-Set-Settings
A common type of setting is a restricted set of string values. As such
json_settings
has a special base class StringSetSetting
. For example, we
might want to define a setting that is a type of vehicle
{
"vehicle_type": "car"
}
import sys
import json
from json_settings import Settings
from json_settings import StringSetSetting
from json_settings import SettingErrorMessage
class MainSettings(Settings):
@Settings.assign
def __init__(self, values):
self.vehicle_type = VehicleTypeSetting
class VehicleTypeSetting(StringSetSetting):
@StringSetSetting.assign
def __init__(self, value):
self.options = [
"car",
"plane",
"boat"
]
if __name__ == "__main__":
with open("my_json_config_file.json", 'r') as f:
values = json.loads(f.read())
try:
my_cool_settings = MainSettings(values)
except SettingErrorMessage as e:
sys.exit(e)
A few things to note:
StringSetSetting
derived classes must have only one attributeoptions
defined in the__init__
method.options
must be alist
ofstr
values.
If a value that is not in the defined list is passed in the JSON file, the following error message is yielded to the user
vehicle_type -> must be one of ['car', 'plane', 'boat']
List-Settings
We want to define a setting that is a list of a single arbitrary type
{
"entries": [
{
"kind": "car",
"cost": 1000
},
{
"kind": "car",
"cost": 3000
},
{
"kind": "plane",
"cost": 10000
}
]
}
import sys
import json
from json_settings import Settings
from json_settings import ListSetting
from json_settings import StringSetSetting
from json_settings import SettingErrorMessage
class MainSettings(Settings):
@Settings.assign
def __init__(self, values):
self.entries = EntryListSetting
class EntryListSetting(ListSetting):
@ListSetting.assign
def __init__(self, values):
self.type = EntrySetting
class EntrySetting(Settings):
@Settings.assign
def __init__(self, values):
self.kind = VehicleTypeSetting
self.cost = int
class VehicleTypeSetting(StringSetSetting):
@StringSetSetting.assign
def __init__(self, value):
self.options = [
"car",
"plane",
"boat"
]
if __name__ == "__main__":
with open("my_json_config_file.json", 'r') as f:
values = json.loads(f.read())
try:
my_cool_settings = MainSettings(values)
except SettingErrorMessage as e:
sys.exit(e)
print(f"Element type: {type(my_cool_settings[0])}")
print(f"First element.kind: {my_cool_settings.entries[0].kind}")
print(f"First element.cost: {my_cool_settings.entries[0].cost}")
print(f"Third element.kind: {my_cool_settings.entries[2].kind}")
print(f"Third element.cost: {my_cool_settings.entries[2].cost}")
The above code will result in the following output
Element type: <class '__main__.EntrySetting'>
First element.kind: car
First element.cost: 1000
Third element.kind: plane
Third element.cost: 10000
If we introduce an error in the configuration file
{
"entries": [
{
"kind": "caravan",
"cost": 1000
},
{
"kind": "car",
"cost": 3000
},
{
"kind": "plane",
"cost": 10000
}
]
}
The following error message will be yielded to the user, noting the element of the list where the error occurred
entries[0] -> kind -> must be one of ['car', 'plane', 'boat']
A few things to note:
ListSettings
derived classes must define only one attributetype
in the__init__
method, which is the type of the values stored.- A list can contain an arbitrary number of elements.
- All elements of the list must adhere to the constrants of all sub elements of any settings objects contain within it.
- Any
ListSetting
derived object behaves like an immutablelist
. - List order from configuration files is maintained.
Dictionary-Settings
We want to define a setting that is a dictionary where all values are of a single arbitrary type
{
"entries": {
"cheap_car": {
"kind": "car",
"cost": 1000
},
"expensive_car": {
"kind": "car",
"cost": 3000
},
"cheap_plane": {
"kind": "plane",
"cost": 10000
}
}
}
import sys
import json
from json_settings import Settings
from json_settings import DictionarySetting
from json_settings import StringSetSetting
from json_settings import SettingErrorMessage
class MainSettings(Settings):
@Settings.assign
def __init__(self, values):
self.entries = EntryListSetting
class EntryListSetting(DictionarySetting):
@DictionarySetting.assign
def __init__(self, values):
self.type = EntrySetting
class EntrySetting(Settings):
@Settings.assign
def __init__(self, values):
self.kind = VehicleTypeSetting
self.cost = int
class VehicleTypeSetting(StringSetSetting):
@StringSetSetting.assign
def __init__(self, value):
self.options = [
"car",
"plane",
"boat"
]
if __name__ == "__main__":
with open("my_json_config_file.json", 'r') as f:
values = json.loads(f.read())
try:
my_cool_settings = MainSettings(values)
except SettingErrorMessage as e:
sys.exit(e)
print(f"Element type: {type(my_cool_settings['cheap_car'])}")
print(f"First element.kind: {my_cool_settings.entries['cheap_car'].kind}")
print(f"First element.cost: {my_cool_settings.entries['cheap_car'].cost}")
print(f"Third element.kind: {my_cool_settings.entries['cheap_plane'].kind}")
print(f"Third element.cost: {my_cool_settings.entries['cheap_plane'].cost}")
The above code will result in the following output
Element type: <class '__main__.EntrySetting'>
First element.kind: car
First element.cost: 1000
Third element.kind: plane
Third element.cost: 10000
If we introduce an error in the configuration file
{
"entries": {
"cheap_car": {
"kind": "fish",
"cost": 1000
},
"expensive_car": {
"kind": "car",
"cost": 3000
},
"cheap_plane": {
"kind": "plane",
"cost": 10000
}
}
}
The following error message will be yielded to the user, noting the key of the dictionary where the error occurred
entries -> cheap_car -> kind -> must be one of ['car', 'plane', 'boat']
A few things to note:
DictionarySettings
derived classes must define only one attributetype
in the__init__
method, which is the type of the values stored.- A dictionary can contain an arbitrary number of key value pairs.
- All values of the dictionary must adhere to the constrants of all sub elements of any settings objects contain within it.
- Any
DictionarySetting
derived object behaves like an immutabledict
. - The key difference between this and a normal
Settings
derived object is the ability for the user to define arbitrary numbers of the same type of object to a configuration file.
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
Built Distribution
File details
Details for the file json_settings-0.1.4.tar.gz
.
File metadata
- Download URL: json_settings-0.1.4.tar.gz
- Upload date:
- Size: 26.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.2.0 pkginfo/1.5.0.1 requests/2.24.0 setuptools/49.2.1 requests-toolbelt/0.9.1 tqdm/4.50.2 CPython/3.9.0
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | a07ff0f13919c526057e5149b684b4101f031545dda70a1b2887f176c0067ed2 |
|
MD5 | 18461d08533acda68f88e41980fce919 |
|
BLAKE2b-256 | 53a8966099dbbf7a0dbc469946baeda1f2c60d78b4d95e464db6daf2b395e44b |
File details
Details for the file json_settings-0.1.4-py3-none-any.whl
.
File metadata
- Download URL: json_settings-0.1.4-py3-none-any.whl
- Upload date:
- Size: 25.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.2.0 pkginfo/1.5.0.1 requests/2.24.0 setuptools/49.2.1 requests-toolbelt/0.9.1 tqdm/4.50.2 CPython/3.9.0
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 61c5ecab85fcbbf48d8d7ea856e32ba109bd378135d8433a6bb52deb63aab004 |
|
MD5 | f1582e9e6cae8f25bdc9287e7f219c1c |
|
BLAKE2b-256 | b4b62e4a787f721a2f2efd4c449673a22ed18442d14b2988b948557f160d3916 |