confit 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
confit-0.2.1/LICENSE ADDED
@@ -0,0 +1,11 @@
1
+ Copyright 2023 Assistance Publique - Hôpitaux de Paris
2
+
3
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4
+
5
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6
+
7
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8
+
9
+ 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10
+
11
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
confit-0.2.1/PKG-INFO ADDED
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.1
2
+ Name: confit
3
+ Version: 0.2.1
4
+ Summary: Smart configuration framework
5
+ Author-email: Perceval Wajsburt <perceval.wajsburt-ext@aphp.fr>, Thomas Petit-Jean <thomas.petitjean@aphp.fr>, Adam Remaki <adam.remaki@aphp.fr>, Alice Calliger <alice.calliger@aphp.fr>
6
+ License: Copyright 2023 Assistance Publique - Hôpitaux de Paris
7
+
8
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
9
+
10
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
13
+
14
+ 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
17
+
18
+ Requires-Python: <4.0,>=3.7.1
19
+ Description-Content-Type: text/markdown
20
+ Provides-Extra: dev
21
+ License-File: LICENSE
22
+
23
+ ![Tests](https://img.shields.io/github/actions/workflow/status/aphp/confit/tests.yml?branch=main&label=tests&style=flat-square)
24
+ [![Documentation](https://img.shields.io/github/actions/workflow/status/aphp/confit/documentation.yml?branch=main&label=docs&style=flat-square)](https://aphp.github.io/confit/latest/)
25
+ [![PyPI](https://img.shields.io/pypi/v/confit?color=blue&style=flat-square)](https://pypi.org/project/confit/)
26
+ [![Codecov](https://img.shields.io/codecov/c/github/aphp/confit?logo=codecov&style=flat-square)](https://codecov.io/gh/aphp/confit)
27
+
28
+
29
+ # Confit
30
+
31
+ Confit is a complete and easy-to-use configuration framework aimed at improving the reproducibility
32
+ of experiments by relying on the Python typing system, minimal configuration files and
33
+ command line interfaces.
34
+
35
+ ## Getting started
36
+
37
+ Install the library with pip:
38
+
39
+ <div class="termy">
40
+
41
+ ```bash
42
+ pip install confit
43
+ ```
44
+
45
+ </div>
46
+
47
+ Confit only abstracts the boilerplate code related to configuration and
48
+ leaves the rest of your code unchanged.
49
+
50
+ Here is an example:
51
+
52
+ <h5 a><strong><code>script.py</code></strong></h5>
53
+
54
+ ```diff
55
+ + from confit import Cli, Registry, RegistryCollection
56
+
57
+ + class registry(RegistryCollection):
58
+ + factory = Registry(("test_cli", "factory"), entry_points=True)
59
+
60
+ + @registry.factory.register("submodel")
61
+ class SubModel:
62
+ # Type hinting is optional but recommended !
63
+ def __init__(self, value: float, desc: str = ""):
64
+ self.value = value
65
+ self.desc = desc
66
+
67
+
68
+ + @registry.factory.register("bigmodel")
69
+ class BigModel:
70
+ def __init__(self, date: datetime.date, submodel: SubModel):
71
+ self.date = date
72
+ self.submodel = submodel
73
+
74
+ + app = Cli(pretty_exceptions_show_locals=False)
75
+
76
+ + @app.command(name="script", registry=registry)
77
+ def func(modelA: BigModel, modelB: BigModel, other: int, seed: int):
78
+ assert modelA.submodel is modelB.submodel
79
+ assert modelA.date == datetime.date(2010, 10, 10)
80
+ print("Other:", other)
81
+
82
+ + if __name__ == "__main__":
83
+ + app()
84
+ ```
85
+
86
+
87
+ Create a new config file
88
+
89
+ <h5 a><strong><code>config.cfg</code></strong></h5>
90
+
91
+ ```ini
92
+ # CLI sections
93
+ [script]
94
+ modelA = ${modelA}
95
+ modelB = ${modelB}
96
+
97
+ # CLI common parameters
98
+ [modelA]
99
+ @factory = "bigmodel"
100
+ date = "2003-02-01"
101
+
102
+ [modelA.submodel]
103
+ @factory = "submodel"
104
+ value = 12
105
+
106
+ [modelB]
107
+ date = "2003-04-05"
108
+ submodel = ${modelA.submodel}
109
+ ```
110
+
111
+ and run the following command from the terminal
112
+
113
+ <div class="termy">
114
+
115
+ ```bash
116
+ python script.py --config config.cfg --seed 42
117
+ ```
118
+
119
+ </div>
120
+
121
+ You can still call the `function` method from your code, but now also benefit from
122
+ argument validation !
123
+
124
+ ```python
125
+ from script import func, BigModel, SubModel
126
+
127
+ # To seed before creating the models
128
+ from confit.utils.random import set_seed
129
+
130
+ seed = 42
131
+ set_seed(seed)
132
+
133
+ submodel = SubModel(value=12)
134
+ # BigModel will cast date strings as datetime.date objects
135
+ modelA = BigModel(date="2003-02-01", submodel=submodel)
136
+ modelB = BigModel(date="2003-04-05", submodel=submodel)
137
+ func(
138
+ modelA=modelA,
139
+ modelB=modelA,
140
+ seed=seed,
141
+ )
142
+ ```
143
+
144
+
145
+ Visit the [documentation](https://aphp.github.io/confit/) for more information!
146
+
147
+ ## Acknowledgement
148
+
149
+ We would like to thank [Assistance Publique – Hôpitaux de Paris](https://www.aphp.fr/)
150
+ and [AP-HP Foundation](https://fondationrechercheaphp.fr/) for funding this project.
confit-0.2.1/README.md ADDED
@@ -0,0 +1,128 @@
1
+ ![Tests](https://img.shields.io/github/actions/workflow/status/aphp/confit/tests.yml?branch=main&label=tests&style=flat-square)
2
+ [![Documentation](https://img.shields.io/github/actions/workflow/status/aphp/confit/documentation.yml?branch=main&label=docs&style=flat-square)](https://aphp.github.io/confit/latest/)
3
+ [![PyPI](https://img.shields.io/pypi/v/confit?color=blue&style=flat-square)](https://pypi.org/project/confit/)
4
+ [![Codecov](https://img.shields.io/codecov/c/github/aphp/confit?logo=codecov&style=flat-square)](https://codecov.io/gh/aphp/confit)
5
+
6
+
7
+ # Confit
8
+
9
+ Confit is a complete and easy-to-use configuration framework aimed at improving the reproducibility
10
+ of experiments by relying on the Python typing system, minimal configuration files and
11
+ command line interfaces.
12
+
13
+ ## Getting started
14
+
15
+ Install the library with pip:
16
+
17
+ <div class="termy">
18
+
19
+ ```bash
20
+ pip install confit
21
+ ```
22
+
23
+ </div>
24
+
25
+ Confit only abstracts the boilerplate code related to configuration and
26
+ leaves the rest of your code unchanged.
27
+
28
+ Here is an example:
29
+
30
+ <h5 a><strong><code>script.py</code></strong></h5>
31
+
32
+ ```diff
33
+ + from confit import Cli, Registry, RegistryCollection
34
+
35
+ + class registry(RegistryCollection):
36
+ + factory = Registry(("test_cli", "factory"), entry_points=True)
37
+
38
+ + @registry.factory.register("submodel")
39
+ class SubModel:
40
+ # Type hinting is optional but recommended !
41
+ def __init__(self, value: float, desc: str = ""):
42
+ self.value = value
43
+ self.desc = desc
44
+
45
+
46
+ + @registry.factory.register("bigmodel")
47
+ class BigModel:
48
+ def __init__(self, date: datetime.date, submodel: SubModel):
49
+ self.date = date
50
+ self.submodel = submodel
51
+
52
+ + app = Cli(pretty_exceptions_show_locals=False)
53
+
54
+ + @app.command(name="script", registry=registry)
55
+ def func(modelA: BigModel, modelB: BigModel, other: int, seed: int):
56
+ assert modelA.submodel is modelB.submodel
57
+ assert modelA.date == datetime.date(2010, 10, 10)
58
+ print("Other:", other)
59
+
60
+ + if __name__ == "__main__":
61
+ + app()
62
+ ```
63
+
64
+
65
+ Create a new config file
66
+
67
+ <h5 a><strong><code>config.cfg</code></strong></h5>
68
+
69
+ ```ini
70
+ # CLI sections
71
+ [script]
72
+ modelA = ${modelA}
73
+ modelB = ${modelB}
74
+
75
+ # CLI common parameters
76
+ [modelA]
77
+ @factory = "bigmodel"
78
+ date = "2003-02-01"
79
+
80
+ [modelA.submodel]
81
+ @factory = "submodel"
82
+ value = 12
83
+
84
+ [modelB]
85
+ date = "2003-04-05"
86
+ submodel = ${modelA.submodel}
87
+ ```
88
+
89
+ and run the following command from the terminal
90
+
91
+ <div class="termy">
92
+
93
+ ```bash
94
+ python script.py --config config.cfg --seed 42
95
+ ```
96
+
97
+ </div>
98
+
99
+ You can still call the `function` method from your code, but now also benefit from
100
+ argument validation !
101
+
102
+ ```python
103
+ from script import func, BigModel, SubModel
104
+
105
+ # To seed before creating the models
106
+ from confit.utils.random import set_seed
107
+
108
+ seed = 42
109
+ set_seed(seed)
110
+
111
+ submodel = SubModel(value=12)
112
+ # BigModel will cast date strings as datetime.date objects
113
+ modelA = BigModel(date="2003-02-01", submodel=submodel)
114
+ modelB = BigModel(date="2003-04-05", submodel=submodel)
115
+ func(
116
+ modelA=modelA,
117
+ modelB=modelA,
118
+ seed=seed,
119
+ )
120
+ ```
121
+
122
+
123
+ Visit the [documentation](https://aphp.github.io/confit/) for more information!
124
+
125
+ ## Acknowledgement
126
+
127
+ We would like to thank [Assistance Publique – Hôpitaux de Paris](https://www.aphp.fr/)
128
+ and [AP-HP Foundation](https://fondationrechercheaphp.fr/) for funding this project.
@@ -0,0 +1,10 @@
1
+ from .cli import Cli # noqa F401
2
+ from .config import Config # noqa F401
3
+ from .registry import (
4
+ Registry, # noqa F401
5
+ get_default_registry, # noqa F401
6
+ set_default_registry, # noqa F401
7
+ RegistryCollection, # noqa F401
8
+ )
9
+
10
+ __version__ = "0.2.1"
@@ -0,0 +1,158 @@
1
+ import inspect
2
+ import sys
3
+ from pathlib import Path
4
+ from typing import Any, Callable, Dict, List, Optional, Type, Union
5
+
6
+ from pydantic import ValidationError
7
+ from typer import Context, Typer, colors, secho
8
+ from typer.core import TyperCommand
9
+ from typer.models import CommandFunctionType, Default
10
+
11
+ from .config import Config, merge_from_disk
12
+ from .registry import validate_arguments
13
+ from .utils.random import set_seed
14
+ from .utils.xjson import loads
15
+
16
+
17
+ def parse_overrides(args: List[str]) -> Dict[str, Any]:
18
+ """
19
+ Parse the overrides from the command line into a dictionary
20
+ of key-value pairs.
21
+
22
+ Parameters
23
+ ----------
24
+ args: List[str]
25
+ The arguments to parse
26
+
27
+ Returns
28
+ -------
29
+ Dict[str, Any]
30
+ The parsed overrides as a dictionary
31
+ """
32
+ result = {}
33
+ while args:
34
+ opt = args.pop(0)
35
+ err = f"Invalid config override '{opt}'"
36
+ if opt.startswith("--"): # new argument
37
+ opt = opt.replace("--", "")
38
+ if "=" in opt: # we have --opt=value
39
+ opt, value = opt.split("=", 1)
40
+ else:
41
+ if not args or args[0].startswith("--"): # flag with no value
42
+ value = "true"
43
+ else:
44
+ value = args.pop(0)
45
+ opt = opt.replace("-", "_")
46
+ result[opt] = loads(value)
47
+ else:
48
+ secho(f"{err}: doesn't support shorthands", fg=colors.RED)
49
+ exit(1)
50
+ return result
51
+
52
+
53
+ class Cli(Typer):
54
+ """
55
+ Custom Typer object that:
56
+
57
+ - validates a command parameters before executing it
58
+ - accepts a configuration file describing the parameters
59
+ - automatically instantiates parameters given a dictionary when type hinted
60
+ """
61
+
62
+ def command( # noqa
63
+ self,
64
+ name,
65
+ *,
66
+ cls: Optional[Type[TyperCommand]] = None,
67
+ context_settings: Optional[Dict[Any, Any]] = None,
68
+ help: Optional[str] = None,
69
+ epilog: Optional[str] = None,
70
+ short_help: Optional[str] = None,
71
+ options_metavar: str = "[OPTIONS]",
72
+ add_help_option: bool = True,
73
+ no_args_is_help: bool = False,
74
+ hidden: bool = False,
75
+ deprecated: bool = False,
76
+ # Rich settings
77
+ rich_help_panel: Union[str, None] = Default(None),
78
+ registry: Any = None,
79
+ ) -> Callable[[CommandFunctionType], CommandFunctionType]:
80
+ typer_command = super().command(
81
+ name=name,
82
+ cls=cls,
83
+ help=help,
84
+ epilog=epilog,
85
+ short_help=short_help,
86
+ options_metavar=options_metavar,
87
+ add_help_option=add_help_option,
88
+ no_args_is_help=no_args_is_help,
89
+ hidden=hidden,
90
+ deprecated=deprecated,
91
+ rich_help_panel=rich_help_panel,
92
+ context_settings={
93
+ **(context_settings or {}),
94
+ "ignore_unknown_options": True,
95
+ "allow_extra_args": True,
96
+ },
97
+ )
98
+
99
+ def wrapper(fn):
100
+ validated = validate_arguments(fn)
101
+
102
+ @typer_command
103
+ def command(ctx: Context, config: Optional[List[Path]] = None):
104
+ config_path = config
105
+
106
+ has_meta = _fn_has_meta(fn)
107
+ if config_path:
108
+ config, name_from_file = merge_from_disk(config_path)
109
+ else:
110
+ config = Config({name: {}})
111
+ for k, v in parse_overrides(ctx.args).items():
112
+ if "." not in k:
113
+ parts = (name, k)
114
+ else:
115
+ parts = k.split(".")
116
+ if (
117
+ parts[0] in validated.model.__fields__
118
+ and parts[0] not in config
119
+ ):
120
+ parts = (name, *parts)
121
+ current = config
122
+ if parts[0] not in current:
123
+ raise Exception(
124
+ f"{k} does not match any existing section in config"
125
+ )
126
+ for part in parts[:-1]:
127
+ current = current.setdefault(part, Config())
128
+ current[parts[-1]] = v
129
+ try:
130
+ resolved_config = config.resolve(registry=registry)
131
+ default_seed = validated.model.__fields__.get("seed")
132
+ seed = config.get(name, {}).get("seed", default_seed)
133
+ if seed is not None:
134
+ set_seed(seed)
135
+ if has_meta:
136
+ config_meta = dict(
137
+ config_path=config_path,
138
+ resolved_config=resolved_config,
139
+ unresolved_config=config,
140
+ )
141
+ return validated(
142
+ **resolved_config.get(name, {}),
143
+ config_meta=config_meta,
144
+ )
145
+ else:
146
+ return validated(**resolved_config.get(name, {}))
147
+ except ValidationError as e:
148
+ print("\x1b[{}m{}\x1b[0m".format("38;5;1", "Validation error"))
149
+ print(str(e))
150
+ sys.exit(1)
151
+
152
+ return validated
153
+
154
+ return wrapper
155
+
156
+
157
+ def _fn_has_meta(fn):
158
+ return "config_meta" in inspect.signature(fn).parameters