diff --git a/README.md b/README.md index 3edc5c7..8961ab6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,1053 @@ # 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 +- [json-settings](#json-settings) +- [Contents](#contents) +- [Installation](#installation) +- [Getting Started](#getting-started) + - [Primitive-Types](#primitive-types) + - [Reference-Types](#reference-types) + - [Null-Types](#null-types) + - [Terminus-Setting](#terminus-setting) + - [Setting-Error-Messages](#setting-error-messages) + - [Number-Settings](#number-settings) + - [Spaces](#spaces) + - [Range-Matching](#range-matching) + - [String-Set-Settings](#string-set-settings) + - [List-Settings](#list-settings) + - [Dictionary-Settings](#dictionary-settings) + + +# 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: + +```json +{ + "my_integer": 1 +} +``` + +So we use the basic unit of json_settings, the `Settings` base class, to define +a new `Settings` derived class +```python +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 + +``` +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 to `self`). +- 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: + +```json +{ + "footware": { + "type": "formal", + "quantity": 2 + }, + "gloves": true +} +``` + +The corresponding Python class structure is as follows: +```python +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_". + +```json +{ + "my_king": "king_james" +} +``` + +The corresponding Python class structure is as follows: +```python +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` and `TypeError` and raise + `SettingCheckError`, which in turn is caught by the enclosing `Settings` + derived instance and raised as a `SettingErrorMessage`. +- The value of the setting in a `TerminusSetting` derived class is stored in the + `value` attribute. +- When an attribute of a `Settings` object which is a `TerminusSetting` derived + type is accessed, the value stored in the `TerminusSetting` derived instances + is returned, NOT the `TerminusSettings` 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 + +```json +{ + "first": { + "second": { + "fourth": 1, + "fith": "fith" + }, + "third": false + } +} +``` + +```python +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 + +```json +{ + "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 + +```json +{ + "first": { + "second": { + "fourth": 1, + "fith": "fith" + }, + "third": 1 + } +} +``` + +we get the following error message +``` +first -> third -> Expecting : | Received: +``` + +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: + +```json +{ + "important_number": 1.0 +} + +``` + +```python +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 bounds + - `lower_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 + +```json +{ + "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 + +```json +{ + "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 + +```json +{ + "primary_number": { + "array": [1.0, 2.0, 3.0] + }, + "secondary_number": { + "min": 4.0, + "max": 6.0, + "num": 3 + } +} +``` + +```python +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. + +```python +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 the `Space` + constructor + - restrict : `dict`[`str`, `str`] + A dictionary of `str`: `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 in `restrict` is found, only subsettings with + name equal to the corresponding value will be searched. + +### 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. + +```json +{ + "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] + } +} +``` + +```python +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 +```json +{ + "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 + +```json +{ + "vehicle_type": "car" +} +``` +```python +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 attribute `options` + defined in the `__init__` method. `options` must be a `list` of `str` 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 + +```json +{ + "entries": [ + { + "kind": "car", + "cost": 1000 + }, + { + "kind": "car", + "cost": 3000 + }, + { + "kind": "plane", + "cost": 10000 + } + ] +} +``` +```python +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: +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 +```json +{ + "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 attribute `type` 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 immutable `list`. +- 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 + +```json +{ + "entries": { + "cheap_car": { + "kind": "car", + "cost": 1000 + }, + "expensive_car": { + "kind": "car", + "cost": 3000 + }, + "cheap_plane": { + "kind": "plane", + "cost": 10000 + } + } +} +``` +```python +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: +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 +```json +{ + "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 attribute `type` 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 immutable `dict`. +- 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. \ No newline at end of file diff --git a/tutorial/dictionary.json b/tutorial/dictionary.json new file mode 100644 index 0000000..f42e80f --- /dev/null +++ b/tutorial/dictionary.json @@ -0,0 +1,16 @@ +{ + "entries": { + "cheap_car": { + "kind": "caofr", + "cost": 1000 + }, + "expensive_car": { + "kind": "car", + "cost": 3000 + }, + "cheap_plane": { + "kind": "plane", + "cost": 10000 + } + } +} \ No newline at end of file diff --git a/tutorial/dictionary.py b/tutorial/dictionary.py new file mode 100644 index 0000000..0d12439 --- /dev/null +++ b/tutorial/dictionary.py @@ -0,0 +1,51 @@ +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 = EntryDictionarySetting + +class EntryDictionarySetting(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("dictionary.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.entries['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}") + \ No newline at end of file diff --git a/tutorial/errormessage.json b/tutorial/errormessage.json new file mode 100644 index 0000000..4a17d2a --- /dev/null +++ b/tutorial/errormessage.json @@ -0,0 +1,9 @@ +{ + "first": { + "second": { + "fourth": 1, + "fith": "f" + }, + "third": 1 + } +} \ No newline at end of file diff --git a/tutorial/errormessage.py b/tutorial/errormessage.py new file mode 100644 index 0000000..a144557 --- /dev/null +++ b/tutorial/errormessage.py @@ -0,0 +1,48 @@ +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("errormessage.json", 'r') as f: + values = json.loads(f.read()) + + print(type(values["first"]["third"])) + + try: + my_cool_settings= MainSettings(values) + except SettingErrorMessage as e: + sys.exit(e) diff --git a/tutorial/list.json b/tutorial/list.json new file mode 100644 index 0000000..add9bb9 --- /dev/null +++ b/tutorial/list.json @@ -0,0 +1,16 @@ +{ + "entries": [ + { + "kind": "car", + "cost": 1000 + }, + { + "kind": "car", + "cost": 3000 + }, + { + "kind": "plane", + "cost": 10000 + } + ] +} \ No newline at end of file diff --git a/tutorial/list.py b/tutorial/list.py new file mode 100644 index 0000000..f3dadd3 --- /dev/null +++ b/tutorial/list.py @@ -0,0 +1,50 @@ +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("list.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.entries[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}") diff --git a/tutorial/match.json b/tutorial/match.json new file mode 100644 index 0000000..e9f7c93 --- /dev/null +++ b/tutorial/match.json @@ -0,0 +1,15 @@ +{ + "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" + } +} \ No newline at end of file diff --git a/tutorial/match.py b/tutorial/match.py new file mode 100644 index 0000000..2da979b --- /dev/null +++ b/tutorial/match.py @@ -0,0 +1,50 @@ +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("match.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)}") \ No newline at end of file diff --git a/tutorial/number.json b/tutorial/number.json new file mode 100644 index 0000000..25304c3 --- /dev/null +++ b/tutorial/number.json @@ -0,0 +1,7 @@ +{ + "important_number": { + "min": -1.0, + "max": 5.0, + "num": 6 + } +} \ No newline at end of file diff --git a/tutorial/number.py b/tutorial/number.py new file mode 100644 index 0000000..676ec6a --- /dev/null +++ b/tutorial/number.py @@ -0,0 +1,34 @@ +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("number.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}") \ No newline at end of file diff --git a/tutorial/primitive.json b/tutorial/primitive.json new file mode 100644 index 0000000..0e0d528 --- /dev/null +++ b/tutorial/primitive.json @@ -0,0 +1,3 @@ +{ + "my_integer": 1 +} \ No newline at end of file diff --git a/tutorial/primitive.py b/tutorial/primitive.py new file mode 100644 index 0000000..e1b6d3c --- /dev/null +++ b/tutorial/primitive.py @@ -0,0 +1,19 @@ +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("primitive.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)) \ No newline at end of file diff --git a/tutorial/reference.json b/tutorial/reference.json new file mode 100644 index 0000000..1e0464e --- /dev/null +++ b/tutorial/reference.json @@ -0,0 +1,7 @@ +{ + "footware": { + "type": "formal", + "quantity": 2 + }, + "gloves": true +} \ No newline at end of file diff --git a/tutorial/reference.py b/tutorial/reference.py new file mode 100644 index 0000000..210ecaf --- /dev/null +++ b/tutorial/reference.py @@ -0,0 +1,28 @@ +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("reference.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) \ No newline at end of file diff --git a/tutorial/space.json b/tutorial/space.json new file mode 100644 index 0000000..895055f --- /dev/null +++ b/tutorial/space.json @@ -0,0 +1,10 @@ +{ + "primary_number": { + "array": [1.0, 2.0, 3.0] + }, + "secondary_number": { + "min": 4.0, + "max": 6.0, + "num": 3 + } +} \ No newline at end of file diff --git a/tutorial/space.py b/tutorial/space.py new file mode 100644 index 0000000..cc9a61a --- /dev/null +++ b/tutorial/space.py @@ -0,0 +1,47 @@ +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("space.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)}") \ No newline at end of file diff --git a/tutorial/stringset.json b/tutorial/stringset.json new file mode 100644 index 0000000..1e952bf --- /dev/null +++ b/tutorial/stringset.json @@ -0,0 +1,3 @@ +{ + "vehicle_type": "fish" +} \ No newline at end of file diff --git a/tutorial/stringset.py b/tutorial/stringset.py new file mode 100644 index 0000000..79dadef --- /dev/null +++ b/tutorial/stringset.py @@ -0,0 +1,34 @@ +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("stringset.json", 'r') as f: + values = json.loads(f.read()) + + try: + my_cool_settings = MainSettings(values) + except SettingErrorMessage as e: + sys.exit(e) + \ No newline at end of file