granular-configuration-language 2.0.0__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.
- granular_configuration_language-2.0.0/PKG-INFO +131 -0
- granular_configuration_language-2.0.0/README.md +104 -0
- granular_configuration_language-2.0.0/granular_configuration_language/__init__.py +12 -0
- granular_configuration_language-2.0.0/granular_configuration_language/_base_path.py +23 -0
- granular_configuration_language-2.0.0/granular_configuration_language/_build.py +94 -0
- granular_configuration_language-2.0.0/granular_configuration_language/_cache.py +105 -0
- granular_configuration_language-2.0.0/granular_configuration_language/_configuration.py +407 -0
- granular_configuration_language-2.0.0/granular_configuration_language/_json.py +72 -0
- granular_configuration_language-2.0.0/granular_configuration_language/_lazy_load_configuration.py +296 -0
- granular_configuration_language-2.0.0/granular_configuration_language/_load.py +28 -0
- granular_configuration_language-2.0.0/granular_configuration_language/_locations.py +121 -0
- granular_configuration_language-2.0.0/granular_configuration_language/_merge.py +59 -0
- granular_configuration_language-2.0.0/granular_configuration_language/_s.py +3 -0
- granular_configuration_language-2.0.0/granular_configuration_language/_utils.py +50 -0
- granular_configuration_language-2.0.0/granular_configuration_language/available_tags.py +14 -0
- granular_configuration_language-2.0.0/granular_configuration_language/exceptions.py +81 -0
- granular_configuration_language-2.0.0/granular_configuration_language/proxy.py +3 -0
- granular_configuration_language-2.0.0/granular_configuration_language/py.typed +0 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/__init__.py +4 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/_tags/__init__.py +7 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/_tags/_date.py +31 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/_tags/_del.py +9 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/_tags/_dict.py +10 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/_tags/_env.py +19 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/_tags/_mask.py +11 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/_tags/_merge.py +20 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/_tags/_parse_env.py +63 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/_tags/_parse_file.py +47 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/_tags/_placeholder.py +10 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/_tags/_ref.py +17 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/_tags/_sub.py +17 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/_tags/_uuid.py +12 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/_tags/func_and_class.py +48 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/classes.py +200 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/decorators/__init__.py +21 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/decorators/_base.py +238 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/decorators/_lazy.py +180 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/decorators/_lazy_eval.py +24 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/decorators/_tag_loader.py +159 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/decorators/_tag_tracker.py +34 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/decorators/_type_checking.py +122 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/decorators/interpolate/__init__.py +7 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/decorators/interpolate/_env_var_parser.py +43 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/decorators/interpolate/_interpolate.py +182 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/decorators/ref/__init__.py +3 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/decorators/ref/_ref.py +72 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/load/__init__.py +3 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/load/_constructors.py +29 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/load/_external.py +45 -0
- granular_configuration_language-2.0.0/granular_configuration_language/yaml/load/_handler.py +39 -0
- granular_configuration_language-2.0.0/pyproject.toml +150 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: granular-configuration-language
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: This configuration utility library allows your code use YAML as a configuration language for internal and external parties, allowing configuration to be crafted from multiple sources and merged just before use, with added YAML Tags that run lazily for added functionality.
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Eric Jensen
|
|
7
|
+
Author-email: eric.jensen42@gmail.com
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Natural Language :: English
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Utilities
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Dist: python-dateutil ; python_version < "3.11"
|
|
20
|
+
Requires-Dist: python-jsonpath
|
|
21
|
+
Requires-Dist: ruamel.yaml (>=0.18)
|
|
22
|
+
Project-URL: Documentation, https://lifedox.github.io/granular-configuration-language/README.html
|
|
23
|
+
Project-URL: Homepage, https://lifedox.github.io/granular-configuration-language/README.html
|
|
24
|
+
Project-URL: Repository, https://github.com/lifedox/granular-configuration-language
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# `granular-configuration-language`
|
|
28
|
+
|
|
29
|
+
[](https://github.com/lifedox/granular-configuration-language/tree/python-coverage-comment-action-data)  
|
|
30
|
+
|
|
31
|
+
> ⚠️ **This library is meant for trusted configuration files.** ⚠️
|
|
32
|
+
|
|
33
|
+
Get started or read more at the [documentation site](https://lifedox.github.io/granular-configuration-language/doc-spec/getting_started.html).
|
|
34
|
+
|
|
35
|
+
## Why does this exist?
|
|
36
|
+
|
|
37
|
+
This library exists to allow your code use YAML as a configuration language for internal and external parties, allowing configuration to be crafted from multiple sources and merged just before use, with added [YAML Tags](https://lifedox.github.io/granular-configuration-language/doc-spec/yaml.html) that run lazily for added functionality, and plugin support for creating custom YAML Tags.
|
|
38
|
+
|
|
39
|
+
Some use cases:
|
|
40
|
+
|
|
41
|
+
- You are writing a library to help connect to some databases. You want users to easily changes settings and defined databases by name.
|
|
42
|
+
- Conceptual Example:
|
|
43
|
+
- Library Code:
|
|
44
|
+
```python
|
|
45
|
+
CONFIG = LazyLoadConfiguration(
|
|
46
|
+
Path(__file___).parent / "config.yaml",
|
|
47
|
+
"./database-util-config.yaml",
|
|
48
|
+
"~/configs/database-util-config.yaml",
|
|
49
|
+
base_path="database-util",
|
|
50
|
+
env_location_var_name="ORG_COMMON_CONFIG_LOCATIONS",
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
- Library configuration:
|
|
54
|
+
```yaml
|
|
55
|
+
database-util:
|
|
56
|
+
common_settings:
|
|
57
|
+
use_decimal: true
|
|
58
|
+
encryption_type: secure
|
|
59
|
+
databases: {} # Empty mapping, for users define
|
|
60
|
+
```
|
|
61
|
+
- User application configuration:
|
|
62
|
+
```yaml
|
|
63
|
+
database-util:
|
|
64
|
+
common_settings:
|
|
65
|
+
use_decimal: false
|
|
66
|
+
databases:
|
|
67
|
+
datebase1:
|
|
68
|
+
location: http://somewhere
|
|
69
|
+
user: !Mask ${DB_USERNAME}
|
|
70
|
+
password: !Mask ${DB_PASSWORD}
|
|
71
|
+
```
|
|
72
|
+
- You are deploying an application that has multiple deployment types with specific settings.
|
|
73
|
+
- Conceptual Example:
|
|
74
|
+
- Library Code:
|
|
75
|
+
```python
|
|
76
|
+
CONFIG = LazyLoadConfiguration(
|
|
77
|
+
Path(__file___).parent / "config.yaml",
|
|
78
|
+
"./database-util-config.yaml",
|
|
79
|
+
base_path="app",
|
|
80
|
+
)
|
|
81
|
+
```
|
|
82
|
+
- Base configuration:
|
|
83
|
+
```yaml
|
|
84
|
+
app:
|
|
85
|
+
log_as: really cool app name
|
|
86
|
+
log_to: nowhere
|
|
87
|
+
```
|
|
88
|
+
- AWS Lambda deploy:
|
|
89
|
+
```yaml
|
|
90
|
+
app:
|
|
91
|
+
log_to: std_out
|
|
92
|
+
```
|
|
93
|
+
- Server deploy:
|
|
94
|
+
```yaml
|
|
95
|
+
app:
|
|
96
|
+
log_to: !Sub file://var/log/${$.app.log_as}.log
|
|
97
|
+
```
|
|
98
|
+
- You are writing a [`pytest`](https://docs.pytest.org/en/stable/) plugin that create test data using named fixtures configured by the user.
|
|
99
|
+
- Conceptual Examples:
|
|
100
|
+
- Library Code:
|
|
101
|
+
```python
|
|
102
|
+
CONFIG = LazyLoadConfiguration(
|
|
103
|
+
Path(__file___).parent / "fixture_config.yaml",
|
|
104
|
+
*Path().rglob("fixture_config.yaml"),
|
|
105
|
+
base_path="fixture-gen",
|
|
106
|
+
).config
|
|
107
|
+
#
|
|
108
|
+
for name, spec in CONFIG.fixtures:
|
|
109
|
+
generate_fixture(name, spec)
|
|
110
|
+
```
|
|
111
|
+
- Library configuration:
|
|
112
|
+
```yaml
|
|
113
|
+
fixture-gen:
|
|
114
|
+
fixtures: {} # Empty mapping, for users define
|
|
115
|
+
```
|
|
116
|
+
- User application configuration:
|
|
117
|
+
```yaml
|
|
118
|
+
fixture-gen:
|
|
119
|
+
fixtures:
|
|
120
|
+
fixture1:
|
|
121
|
+
api: does something
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Why the long name?
|
|
125
|
+
|
|
126
|
+
- It's "granular" because you can specify settings across multiple files at a fine granularity for overriding values.
|
|
127
|
+
- It is meant for trusted "configuration" files.
|
|
128
|
+
- Including "language" make it clear that this is not the source of configuration.
|
|
129
|
+
- A valid piece of feedback was that it sounded like it was the source for configuration, not the processing of generic configuration files.
|
|
130
|
+
- "Format" sounded weirder than "language".
|
|
131
|
+
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# `granular-configuration-language`
|
|
2
|
+
|
|
3
|
+
[](https://github.com/lifedox/granular-configuration-language/tree/python-coverage-comment-action-data)  
|
|
4
|
+
|
|
5
|
+
> ⚠️ **This library is meant for trusted configuration files.** ⚠️
|
|
6
|
+
|
|
7
|
+
Get started or read more at the [documentation site](https://lifedox.github.io/granular-configuration-language/doc-spec/getting_started.html).
|
|
8
|
+
|
|
9
|
+
## Why does this exist?
|
|
10
|
+
|
|
11
|
+
This library exists to allow your code use YAML as a configuration language for internal and external parties, allowing configuration to be crafted from multiple sources and merged just before use, with added [YAML Tags](https://lifedox.github.io/granular-configuration-language/doc-spec/yaml.html) that run lazily for added functionality, and plugin support for creating custom YAML Tags.
|
|
12
|
+
|
|
13
|
+
Some use cases:
|
|
14
|
+
|
|
15
|
+
- You are writing a library to help connect to some databases. You want users to easily changes settings and defined databases by name.
|
|
16
|
+
- Conceptual Example:
|
|
17
|
+
- Library Code:
|
|
18
|
+
```python
|
|
19
|
+
CONFIG = LazyLoadConfiguration(
|
|
20
|
+
Path(__file___).parent / "config.yaml",
|
|
21
|
+
"./database-util-config.yaml",
|
|
22
|
+
"~/configs/database-util-config.yaml",
|
|
23
|
+
base_path="database-util",
|
|
24
|
+
env_location_var_name="ORG_COMMON_CONFIG_LOCATIONS",
|
|
25
|
+
)
|
|
26
|
+
```
|
|
27
|
+
- Library configuration:
|
|
28
|
+
```yaml
|
|
29
|
+
database-util:
|
|
30
|
+
common_settings:
|
|
31
|
+
use_decimal: true
|
|
32
|
+
encryption_type: secure
|
|
33
|
+
databases: {} # Empty mapping, for users define
|
|
34
|
+
```
|
|
35
|
+
- User application configuration:
|
|
36
|
+
```yaml
|
|
37
|
+
database-util:
|
|
38
|
+
common_settings:
|
|
39
|
+
use_decimal: false
|
|
40
|
+
databases:
|
|
41
|
+
datebase1:
|
|
42
|
+
location: http://somewhere
|
|
43
|
+
user: !Mask ${DB_USERNAME}
|
|
44
|
+
password: !Mask ${DB_PASSWORD}
|
|
45
|
+
```
|
|
46
|
+
- You are deploying an application that has multiple deployment types with specific settings.
|
|
47
|
+
- Conceptual Example:
|
|
48
|
+
- Library Code:
|
|
49
|
+
```python
|
|
50
|
+
CONFIG = LazyLoadConfiguration(
|
|
51
|
+
Path(__file___).parent / "config.yaml",
|
|
52
|
+
"./database-util-config.yaml",
|
|
53
|
+
base_path="app",
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
- Base configuration:
|
|
57
|
+
```yaml
|
|
58
|
+
app:
|
|
59
|
+
log_as: really cool app name
|
|
60
|
+
log_to: nowhere
|
|
61
|
+
```
|
|
62
|
+
- AWS Lambda deploy:
|
|
63
|
+
```yaml
|
|
64
|
+
app:
|
|
65
|
+
log_to: std_out
|
|
66
|
+
```
|
|
67
|
+
- Server deploy:
|
|
68
|
+
```yaml
|
|
69
|
+
app:
|
|
70
|
+
log_to: !Sub file://var/log/${$.app.log_as}.log
|
|
71
|
+
```
|
|
72
|
+
- You are writing a [`pytest`](https://docs.pytest.org/en/stable/) plugin that create test data using named fixtures configured by the user.
|
|
73
|
+
- Conceptual Examples:
|
|
74
|
+
- Library Code:
|
|
75
|
+
```python
|
|
76
|
+
CONFIG = LazyLoadConfiguration(
|
|
77
|
+
Path(__file___).parent / "fixture_config.yaml",
|
|
78
|
+
*Path().rglob("fixture_config.yaml"),
|
|
79
|
+
base_path="fixture-gen",
|
|
80
|
+
).config
|
|
81
|
+
#
|
|
82
|
+
for name, spec in CONFIG.fixtures:
|
|
83
|
+
generate_fixture(name, spec)
|
|
84
|
+
```
|
|
85
|
+
- Library configuration:
|
|
86
|
+
```yaml
|
|
87
|
+
fixture-gen:
|
|
88
|
+
fixtures: {} # Empty mapping, for users define
|
|
89
|
+
```
|
|
90
|
+
- User application configuration:
|
|
91
|
+
```yaml
|
|
92
|
+
fixture-gen:
|
|
93
|
+
fixtures:
|
|
94
|
+
fixture1:
|
|
95
|
+
api: does something
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Why the long name?
|
|
99
|
+
|
|
100
|
+
- It's "granular" because you can specify settings across multiple files at a fine granularity for overriding values.
|
|
101
|
+
- It is meant for trusted "configuration" files.
|
|
102
|
+
- Including "language" make it clear that this is not the source of configuration.
|
|
103
|
+
- A valid piece of feedback was that it sounded like it was the source for configuration, not the processing of generic configuration files.
|
|
104
|
+
- "Format" sounded weirder than "language".
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# isort:skip_file
|
|
2
|
+
import granular_configuration_language.yaml.classes # Needs to import before _configuration to prevent circular import
|
|
3
|
+
from granular_configuration_language._configuration import Configuration, MutableConfiguration
|
|
4
|
+
from granular_configuration_language._lazy_load_configuration import (
|
|
5
|
+
LazyLoadConfiguration,
|
|
6
|
+
LazyLoadConfiguration as LLC,
|
|
7
|
+
MutableLazyLoadConfiguration,
|
|
8
|
+
)
|
|
9
|
+
from granular_configuration_language._merge import merge # depends on LazyLoadConfiguration
|
|
10
|
+
|
|
11
|
+
from granular_configuration_language._json import json_default
|
|
12
|
+
from granular_configuration_language.yaml import Masked, Placeholder
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import collections.abc as tabc
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BasePathPart(str):
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BasePath(tuple[BasePathPart]):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def read_base_path(base_path: str | tabc.Sequence[str] | None) -> BasePath:
|
|
15
|
+
if isinstance(base_path, str):
|
|
16
|
+
if base_path.startswith("/"):
|
|
17
|
+
return BasePath(map(BasePathPart, filter(None, base_path.split("/"))))
|
|
18
|
+
else:
|
|
19
|
+
return BasePath((BasePathPart(base_path),))
|
|
20
|
+
elif base_path:
|
|
21
|
+
return BasePath(map(BasePathPart, base_path))
|
|
22
|
+
else:
|
|
23
|
+
return BasePath()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import collections.abc as tabc
|
|
4
|
+
import typing as typ
|
|
5
|
+
from functools import partial
|
|
6
|
+
from itertools import chain
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from granular_configuration_language import Configuration
|
|
10
|
+
from granular_configuration_language._load import load_file
|
|
11
|
+
from granular_configuration_language._s import setter_secret
|
|
12
|
+
from granular_configuration_language._utils import consume
|
|
13
|
+
from granular_configuration_language.yaml import LazyRoot
|
|
14
|
+
from granular_configuration_language.yaml.load import obj_pairs_func
|
|
15
|
+
|
|
16
|
+
_C = typ.TypeVar("_C", bound=Configuration)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _merge_into_base(configuration_type: typ.Type[_C], base_dict: _C, from_dict: _C) -> None:
|
|
20
|
+
for key, value in from_dict._raw_items():
|
|
21
|
+
if isinstance(value, configuration_type) and (key in base_dict):
|
|
22
|
+
if base_dict.exists(key):
|
|
23
|
+
new_dict = base_dict[key]
|
|
24
|
+
else: # If Placeholder
|
|
25
|
+
new_dict = configuration_type()
|
|
26
|
+
|
|
27
|
+
if isinstance(new_dict, configuration_type):
|
|
28
|
+
_merge_into_base(configuration_type, new_dict, value)
|
|
29
|
+
value = new_dict
|
|
30
|
+
|
|
31
|
+
base_dict._private_set(key, value, setter_secret)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _merge(configuration_type: typ.Type[_C], base_config: _C, configs: tabc.Iterable[_C]) -> _C:
|
|
35
|
+
consume(map(partial(_merge_into_base, configuration_type, base_config), configs))
|
|
36
|
+
return base_config
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _load_configs_from_locations(
|
|
40
|
+
configuration_type: typ.Type[_C], locations: tabc.Iterable[Path], lazy_root: LazyRoot, mutable: bool
|
|
41
|
+
) -> tabc.Iterator[_C]:
|
|
42
|
+
def configuration_only(
|
|
43
|
+
configs: tabc.Iterable[_C | typ.Any],
|
|
44
|
+
) -> tabc.Iterator[_C]:
|
|
45
|
+
for config in configs:
|
|
46
|
+
if isinstance(config, configuration_type):
|
|
47
|
+
yield config
|
|
48
|
+
|
|
49
|
+
_load_file = partial(load_file, lazy_root=lazy_root, mutable=mutable)
|
|
50
|
+
return configuration_only(map(_load_file, locations))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _inject(*new: Configuration, configs: tabc.Iterator[_C], in_front: bool) -> tabc.Iterator[_C]:
|
|
54
|
+
cast = typ.cast(_C, new)
|
|
55
|
+
if in_front:
|
|
56
|
+
return chain(cast, configs)
|
|
57
|
+
else:
|
|
58
|
+
return chain(configs, cast)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _inject_configs(
|
|
62
|
+
configs: tabc.Iterator[_C],
|
|
63
|
+
*,
|
|
64
|
+
before: Configuration | None,
|
|
65
|
+
after: Configuration | None,
|
|
66
|
+
) -> tabc.Iterator[_C]:
|
|
67
|
+
if before and isinstance(before, Configuration):
|
|
68
|
+
configs = _inject(before, configs=configs, in_front=True)
|
|
69
|
+
|
|
70
|
+
if after and isinstance(after, Configuration):
|
|
71
|
+
configs = _inject(after, configs=configs, in_front=False)
|
|
72
|
+
|
|
73
|
+
return configs
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def build_configuration(
|
|
77
|
+
locations: tabc.Iterable[Path],
|
|
78
|
+
mutable: bool,
|
|
79
|
+
*,
|
|
80
|
+
inject_before: Configuration | None,
|
|
81
|
+
inject_after: Configuration | None,
|
|
82
|
+
) -> Configuration:
|
|
83
|
+
configuration_type = obj_pairs_func(mutable)
|
|
84
|
+
base_config = configuration_type()
|
|
85
|
+
lazy_root = LazyRoot.with_root(base_config)
|
|
86
|
+
|
|
87
|
+
valid_configs = _inject_configs(
|
|
88
|
+
_load_configs_from_locations(configuration_type, locations, lazy_root, mutable),
|
|
89
|
+
before=inject_before,
|
|
90
|
+
after=inject_after,
|
|
91
|
+
)
|
|
92
|
+
merged_config = _merge(configuration_type, base_config, valid_configs)
|
|
93
|
+
|
|
94
|
+
return merged_config
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import collections.abc as tabc
|
|
4
|
+
import dataclasses
|
|
5
|
+
import operator as op
|
|
6
|
+
import typing as typ
|
|
7
|
+
from collections import deque
|
|
8
|
+
from functools import cached_property, reduce
|
|
9
|
+
from threading import Lock
|
|
10
|
+
from weakref import WeakValueDictionary
|
|
11
|
+
|
|
12
|
+
from granular_configuration_language._base_path import BasePath, read_base_path
|
|
13
|
+
from granular_configuration_language._build import build_configuration
|
|
14
|
+
from granular_configuration_language._configuration import Configuration
|
|
15
|
+
from granular_configuration_language._locations import Locations
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclasses.dataclass(frozen=False, eq=False, kw_only=True)
|
|
19
|
+
class SharedConfigurationReference:
|
|
20
|
+
_locations: Locations
|
|
21
|
+
_mutable_config: bool
|
|
22
|
+
_inject_before: Configuration | None = None
|
|
23
|
+
_inject_after: Configuration | None = None
|
|
24
|
+
__lock: Lock | None = dataclasses.field(repr=False, compare=False, init=False, default_factory=Lock)
|
|
25
|
+
__notes: deque[NoteOfIntentToRead] = dataclasses.field(repr=False, compare=False, init=False, default_factory=deque)
|
|
26
|
+
|
|
27
|
+
def register(self, note: NoteOfIntentToRead) -> None:
|
|
28
|
+
self.__notes.append(note)
|
|
29
|
+
|
|
30
|
+
def __clear_notes(self, caller: NoteOfIntentToRead) -> None:
|
|
31
|
+
while self.__notes:
|
|
32
|
+
note = self.__notes.pop()
|
|
33
|
+
if note is not caller:
|
|
34
|
+
note._config
|
|
35
|
+
|
|
36
|
+
def build(self, caller: NoteOfIntentToRead) -> Configuration:
|
|
37
|
+
# Making cached_property thread-safe
|
|
38
|
+
if self.__lock:
|
|
39
|
+
with self.__lock:
|
|
40
|
+
self.__config
|
|
41
|
+
self.__lock = None
|
|
42
|
+
self.__clear_notes(caller)
|
|
43
|
+
|
|
44
|
+
return self.__config
|
|
45
|
+
|
|
46
|
+
@cached_property
|
|
47
|
+
def __config(self) -> Configuration:
|
|
48
|
+
return build_configuration(
|
|
49
|
+
self._locations, self._mutable_config, inject_after=self._inject_after, inject_before=self._inject_before
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclasses.dataclass(frozen=False, eq=False, kw_only=True)
|
|
54
|
+
class NoteOfIntentToRead:
|
|
55
|
+
_base_path: BasePath
|
|
56
|
+
_config_ref: SharedConfigurationReference
|
|
57
|
+
|
|
58
|
+
def __post_init__(self) -> None:
|
|
59
|
+
self._config_ref.register(self)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def config(self) -> Configuration:
|
|
63
|
+
config = self._config
|
|
64
|
+
if isinstance(config, Exception):
|
|
65
|
+
raise config
|
|
66
|
+
else:
|
|
67
|
+
return config
|
|
68
|
+
|
|
69
|
+
@cached_property
|
|
70
|
+
def _config(self) -> Configuration | Exception:
|
|
71
|
+
config = self._config_ref.build(self)
|
|
72
|
+
try:
|
|
73
|
+
return reduce(op.getitem, self._base_path, config)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
return e
|
|
76
|
+
finally:
|
|
77
|
+
del self._config_ref
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
store: typ.Final[WeakValueDictionary[Locations, SharedConfigurationReference]] = WeakValueDictionary()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def prepare_to_load_configuration(
|
|
84
|
+
*,
|
|
85
|
+
locations: Locations,
|
|
86
|
+
base_path: str | tabc.Sequence[str] | None,
|
|
87
|
+
mutable_configuration: bool,
|
|
88
|
+
disable_cache: bool,
|
|
89
|
+
inject_before: Configuration | None,
|
|
90
|
+
inject_after: Configuration | None,
|
|
91
|
+
) -> NoteOfIntentToRead:
|
|
92
|
+
if disable_cache or mutable_configuration or inject_after or inject_before:
|
|
93
|
+
shared_config_ref = SharedConfigurationReference(
|
|
94
|
+
_locations=locations,
|
|
95
|
+
_mutable_config=mutable_configuration,
|
|
96
|
+
_inject_after=inject_after,
|
|
97
|
+
_inject_before=inject_before,
|
|
98
|
+
)
|
|
99
|
+
elif locations not in store:
|
|
100
|
+
shared_config_ref = SharedConfigurationReference(_locations=locations, _mutable_config=mutable_configuration)
|
|
101
|
+
store[locations] = shared_config_ref
|
|
102
|
+
else:
|
|
103
|
+
shared_config_ref = store[locations]
|
|
104
|
+
|
|
105
|
+
return NoteOfIntentToRead(_base_path=read_base_path(base_path), _config_ref=shared_config_ref)
|