fmu-settings 0.4.0__py3-none-any.whl → 0.5.1__py3-none-any.whl
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.
Potentially problematic release.
This version of fmu-settings might be problematic. Click here for more details.
- fmu/settings/_global_config.py +57 -15
- fmu/settings/_resources/config_managers.py +15 -176
- fmu/settings/_resources/pydantic_resource_manager.py +179 -8
- fmu/settings/_version.py +2 -2
- {fmu_settings-0.4.0.dist-info → fmu_settings-0.5.1.dist-info}/METADATA +1 -1
- {fmu_settings-0.4.0.dist-info → fmu_settings-0.5.1.dist-info}/RECORD +9 -9
- {fmu_settings-0.4.0.dist-info → fmu_settings-0.5.1.dist-info}/WHEEL +0 -0
- {fmu_settings-0.4.0.dist-info → fmu_settings-0.5.1.dist-info}/licenses/LICENSE +0 -0
- {fmu_settings-0.4.0.dist-info → fmu_settings-0.5.1.dist-info}/top_level.txt +0 -0
fmu/settings/_global_config.py
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Final
|
|
5
5
|
|
|
6
|
+
from pydantic import ValidationError
|
|
7
|
+
|
|
6
8
|
from fmu.config.utilities import yaml_load
|
|
7
9
|
from fmu.datamodels.fmu_results.global_configuration import GlobalConfiguration
|
|
8
10
|
|
|
@@ -10,6 +12,15 @@ from ._logging import null_logger
|
|
|
10
12
|
|
|
11
13
|
logger: Final = null_logger(__name__)
|
|
12
14
|
|
|
15
|
+
|
|
16
|
+
class InvalidGlobalConfigurationError(ValueError):
|
|
17
|
+
"""Raised when a GlobalConfiguration contains invalid or disallowed content.
|
|
18
|
+
|
|
19
|
+
This includes Drogon test data or other disallowed masterdata.
|
|
20
|
+
This error is only raised when strict validation is enabled.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
13
24
|
# These should all be normalized to lower case.
|
|
14
25
|
INVALID_NAMES: Final[tuple[str, ...]] = (
|
|
15
26
|
"drogon",
|
|
@@ -47,53 +58,66 @@ def validate_global_configuration_strictly(cfg: GlobalConfiguration) -> None: #
|
|
|
47
58
|
cfg: A GlobalConfiguration instance to be validated
|
|
48
59
|
|
|
49
60
|
Raises:
|
|
50
|
-
|
|
61
|
+
InvalidGlobalConfigurationError: If some value in the GlobalConfiguration
|
|
62
|
+
is invalid or not allowed
|
|
51
63
|
"""
|
|
52
64
|
# Check model and access
|
|
53
65
|
if cfg.model.name.lower() in INVALID_NAMES:
|
|
54
|
-
raise
|
|
66
|
+
raise InvalidGlobalConfigurationError(
|
|
67
|
+
f"Invalid name in 'model': {cfg.model.name}"
|
|
68
|
+
)
|
|
55
69
|
if cfg.access.asset.name.lower() in INVALID_NAMES:
|
|
56
|
-
raise
|
|
70
|
+
raise InvalidGlobalConfigurationError(
|
|
71
|
+
f"Invalid name in 'access.asset': {cfg.access.asset.name}"
|
|
72
|
+
)
|
|
57
73
|
|
|
58
74
|
# Check masterdata
|
|
59
75
|
|
|
60
76
|
# smda.country
|
|
61
77
|
for country in cfg.masterdata.smda.country:
|
|
62
78
|
if str(country.uuid) in INVALID_UUIDS:
|
|
63
|
-
raise
|
|
79
|
+
raise InvalidGlobalConfigurationError(
|
|
80
|
+
f"Invalid SMDA UUID in 'smda.country': {country.uuid}"
|
|
81
|
+
)
|
|
64
82
|
|
|
65
83
|
# smda.discovery
|
|
66
84
|
for discovery in cfg.masterdata.smda.discovery:
|
|
67
85
|
if discovery.short_identifier.lower() in INVALID_NAMES:
|
|
68
|
-
raise
|
|
86
|
+
raise InvalidGlobalConfigurationError(
|
|
69
87
|
f"Invalid SMDA short identifier in 'smda.discovery': "
|
|
70
88
|
f"{discovery.short_identifier}"
|
|
71
89
|
)
|
|
72
90
|
if str(discovery.uuid) in INVALID_UUIDS:
|
|
73
|
-
raise
|
|
91
|
+
raise InvalidGlobalConfigurationError(
|
|
92
|
+
f"Invalid SMDA UUID in 'smda.discovery': {discovery.uuid}"
|
|
93
|
+
)
|
|
74
94
|
|
|
75
95
|
# smda.field
|
|
76
96
|
for field in cfg.masterdata.smda.field:
|
|
77
97
|
if field.identifier.lower() in INVALID_NAMES:
|
|
78
|
-
raise
|
|
98
|
+
raise InvalidGlobalConfigurationError(
|
|
79
99
|
f"Invalid SMDA identifier in 'smda.field': {field.identifier}"
|
|
80
100
|
)
|
|
81
101
|
if str(field.uuid) in INVALID_UUIDS:
|
|
82
|
-
raise
|
|
102
|
+
raise InvalidGlobalConfigurationError(
|
|
103
|
+
f"Invalid SMDA UUID in 'smda.field': {field.uuid}"
|
|
104
|
+
)
|
|
83
105
|
|
|
84
106
|
# smda.coordinate_system
|
|
85
107
|
if (coord_uuid := str(cfg.masterdata.smda.coordinate_system.uuid)) in INVALID_UUIDS:
|
|
86
|
-
raise
|
|
108
|
+
raise InvalidGlobalConfigurationError(
|
|
109
|
+
f"Invalid SMDA UUID in 'smda.coordinate_system': {coord_uuid}"
|
|
110
|
+
)
|
|
87
111
|
|
|
88
112
|
# smda.stratigraphic_column
|
|
89
113
|
strat = cfg.masterdata.smda.stratigraphic_column
|
|
90
114
|
if strat.identifier.lower() in INVALID_NAMES:
|
|
91
|
-
raise
|
|
115
|
+
raise InvalidGlobalConfigurationError(
|
|
92
116
|
f"Invalid SMDA identifier in 'smda.stratigraphic_column': "
|
|
93
117
|
f"{strat.identifier}"
|
|
94
118
|
)
|
|
95
119
|
if str(strat.uuid) in INVALID_UUIDS:
|
|
96
|
-
raise
|
|
120
|
+
raise InvalidGlobalConfigurationError(
|
|
97
121
|
f"Invalid SMDA UUID in 'smda.stratigraphic_column': {strat.uuid}"
|
|
98
122
|
)
|
|
99
123
|
|
|
@@ -102,7 +126,7 @@ def validate_global_configuration_strictly(cfg: GlobalConfiguration) -> None: #
|
|
|
102
126
|
if cfg.stratigraphy:
|
|
103
127
|
for key in cfg.stratigraphy:
|
|
104
128
|
if key.lower() in INVALID_STRAT_NAMES:
|
|
105
|
-
raise
|
|
129
|
+
raise InvalidGlobalConfigurationError(
|
|
106
130
|
f"Invalid stratigraphy name in 'smda.stratigraphy': {key}"
|
|
107
131
|
)
|
|
108
132
|
|
|
@@ -121,15 +145,22 @@ def load_global_configuration_if_present(
|
|
|
121
145
|
fmu_load: Whether or not to load in the custom 'fmu' format. Default False.
|
|
122
146
|
|
|
123
147
|
Returns:
|
|
124
|
-
GlobalConfiguration instance or None.
|
|
148
|
+
GlobalConfiguration instance or None if file cannot be loaded.
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
ValidationError: If the file is loaded but has invalid schema.
|
|
125
152
|
"""
|
|
126
153
|
loader = "fmu" if fmu_load else "standard"
|
|
127
154
|
try:
|
|
128
155
|
global_variables_dict = yaml_load(path, loader=loader)
|
|
129
156
|
global_config = GlobalConfiguration.model_validate(global_variables_dict)
|
|
130
157
|
logger.debug(f"Global variables at {path} has valid settings data")
|
|
131
|
-
except
|
|
132
|
-
|
|
158
|
+
except ValidationError:
|
|
159
|
+
raise
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.debug(
|
|
162
|
+
f"Failed to load global variables at {path}: {type(e).__name__}: {e}"
|
|
163
|
+
)
|
|
133
164
|
return None
|
|
134
165
|
return global_config
|
|
135
166
|
|
|
@@ -144,6 +175,9 @@ def _find_global_variables_file(paths: list[Path]) -> GlobalConfiguration | None
|
|
|
144
175
|
|
|
145
176
|
Returns:
|
|
146
177
|
A validated GlobalConfiguration or None.
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
ValidationError: If a file is found but has invalid schema.
|
|
147
181
|
"""
|
|
148
182
|
for path in paths:
|
|
149
183
|
if not path.exists():
|
|
@@ -175,6 +209,9 @@ def _find_global_config_file(paths: list[Path]) -> GlobalConfiguration | None:
|
|
|
175
209
|
|
|
176
210
|
Returns:
|
|
177
211
|
A validated GlobalConfiguration or None.
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
ValidationError: If a file is found but has invalid schema.
|
|
178
215
|
"""
|
|
179
216
|
for path in paths:
|
|
180
217
|
if not path.exists():
|
|
@@ -212,6 +249,11 @@ def find_global_config(
|
|
|
212
249
|
|
|
213
250
|
Returns:
|
|
214
251
|
A valid GlobalConfiguration instance, or None.
|
|
252
|
+
|
|
253
|
+
Raises:
|
|
254
|
+
ValidationError: If a configuration file is found but has invalid schema.
|
|
255
|
+
InvalidGlobalConfigurationError: If strict=True and configuration contains
|
|
256
|
+
disallowed content (e.g., Drogon data).
|
|
215
257
|
"""
|
|
216
258
|
base_path = Path(base_path)
|
|
217
259
|
|
|
@@ -3,208 +3,47 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import TYPE_CHECKING,
|
|
7
|
-
|
|
8
|
-
from pydantic import ValidationError
|
|
6
|
+
from typing import TYPE_CHECKING, Final, Self
|
|
9
7
|
|
|
10
8
|
from fmu.settings._logging import null_logger
|
|
11
9
|
from fmu.settings.models.project_config import ProjectConfig
|
|
12
10
|
from fmu.settings.models.user_config import UserConfig
|
|
13
|
-
from fmu.settings.types import ResettableBaseModel # noqa: TC001
|
|
14
11
|
|
|
15
|
-
from .pydantic_resource_manager import
|
|
12
|
+
from .pydantic_resource_manager import (
|
|
13
|
+
MutablePydanticResourceManager,
|
|
14
|
+
)
|
|
16
15
|
|
|
17
16
|
if TYPE_CHECKING:
|
|
18
17
|
# Avoid circular dependency for type hint in __init__ only
|
|
19
18
|
from fmu.settings._fmu_dir import (
|
|
20
|
-
FMUDirectoryBase,
|
|
21
19
|
ProjectFMUDirectory,
|
|
22
20
|
UserFMUDirectory,
|
|
23
21
|
)
|
|
24
22
|
|
|
25
23
|
logger: Final = null_logger(__name__)
|
|
26
24
|
|
|
27
|
-
T = TypeVar("T", bound=ResettableBaseModel)
|
|
28
|
-
|
|
29
25
|
|
|
30
|
-
class
|
|
31
|
-
"""Manages the .fmu configuration file."""
|
|
26
|
+
class ProjectConfigManager(MutablePydanticResourceManager[ProjectConfig]):
|
|
27
|
+
"""Manages the .fmu configuration file in a project."""
|
|
32
28
|
|
|
33
|
-
def __init__(self: Self, fmu_dir:
|
|
34
|
-
"""Initializes the
|
|
35
|
-
super().__init__(fmu_dir,
|
|
29
|
+
def __init__(self: Self, fmu_dir: ProjectFMUDirectory) -> None:
|
|
30
|
+
"""Initializes the ProjectConfig resource manager."""
|
|
31
|
+
super().__init__(fmu_dir, ProjectConfig)
|
|
36
32
|
|
|
37
33
|
@property
|
|
38
34
|
def relative_path(self: Self) -> Path:
|
|
39
35
|
"""Returns the relative path to the config file."""
|
|
40
36
|
return Path("config.json")
|
|
41
37
|
|
|
42
|
-
def _get_dot_notation_key(
|
|
43
|
-
self: Self, config_dict: dict[str, Any], key: str, default: Any = None
|
|
44
|
-
) -> Any:
|
|
45
|
-
"""Sets the value to a dot-notation key.
|
|
46
|
-
|
|
47
|
-
Args:
|
|
48
|
-
config_dict: The configuration dictionary we are modifying (by reference)
|
|
49
|
-
key: The key to set
|
|
50
|
-
default: Value to return if key is not found. Default None
|
|
51
|
-
|
|
52
|
-
Returns:
|
|
53
|
-
The value or default
|
|
54
|
-
"""
|
|
55
|
-
parts = key.split(".")
|
|
56
|
-
value = config_dict
|
|
57
|
-
for part in parts:
|
|
58
|
-
if isinstance(value, dict) and part in value:
|
|
59
|
-
value = value[part]
|
|
60
|
-
else:
|
|
61
|
-
return default
|
|
62
|
-
|
|
63
|
-
return value
|
|
64
|
-
|
|
65
|
-
def get(self: Self, key: str, default: Any = None) -> Any:
|
|
66
|
-
"""Gets a configuration value by key.
|
|
67
|
-
|
|
68
|
-
Supports dot notation for nested values (e.g., "foo.bar")
|
|
69
|
-
|
|
70
|
-
Args:
|
|
71
|
-
key: The configuration key
|
|
72
|
-
default: Value to return if key is not found. Default None
|
|
73
|
-
|
|
74
|
-
Returns:
|
|
75
|
-
The configuration value or default
|
|
76
|
-
"""
|
|
77
|
-
try:
|
|
78
|
-
config = self.load()
|
|
79
|
-
|
|
80
|
-
if "." in key:
|
|
81
|
-
return self._get_dot_notation_key(config.model_dump(), key, default)
|
|
82
|
-
|
|
83
|
-
if hasattr(config, key):
|
|
84
|
-
return getattr(config, key)
|
|
85
|
-
|
|
86
|
-
config_dict = config.model_dump()
|
|
87
|
-
return config_dict.get(key, default)
|
|
88
|
-
except FileNotFoundError as e:
|
|
89
|
-
raise FileNotFoundError(
|
|
90
|
-
f"Resource file for '{self.__class__.__name__}' not found "
|
|
91
|
-
f"at: '{self.path}' when getting key {key}"
|
|
92
|
-
) from e
|
|
93
|
-
|
|
94
|
-
def _set_dot_notation_key(
|
|
95
|
-
self: Self, config_dict: dict[str, Any], key: str, value: Any
|
|
96
|
-
) -> None:
|
|
97
|
-
"""Sets the value to a dot-notation key.
|
|
98
|
-
|
|
99
|
-
Args:
|
|
100
|
-
config_dict: The configuration dictionary we are modifying (by reference)
|
|
101
|
-
key: The key to set
|
|
102
|
-
value: The value to set
|
|
103
|
-
"""
|
|
104
|
-
parts = key.split(".")
|
|
105
|
-
target = config_dict
|
|
106
|
-
|
|
107
|
-
for part in parts[:-1]:
|
|
108
|
-
if part not in target or not isinstance(target[part], dict):
|
|
109
|
-
target[part] = {}
|
|
110
|
-
target = target[part]
|
|
111
|
-
|
|
112
|
-
target[parts[-1]] = value
|
|
113
|
-
|
|
114
|
-
def set(self: Self, key: str, value: Any) -> None:
|
|
115
|
-
"""Sets a configuration value by key.
|
|
116
|
-
|
|
117
|
-
Args:
|
|
118
|
-
key: The configuration key
|
|
119
|
-
value: The value to set
|
|
120
|
-
|
|
121
|
-
Raises:
|
|
122
|
-
FileNotFoundError: If config file doesn't exist
|
|
123
|
-
ValueError: If the updated config is invalid
|
|
124
|
-
"""
|
|
125
|
-
try:
|
|
126
|
-
config = self.load()
|
|
127
|
-
config_dict = config.model_dump()
|
|
128
|
-
|
|
129
|
-
if "." in key:
|
|
130
|
-
self._set_dot_notation_key(config_dict, key, value)
|
|
131
|
-
else:
|
|
132
|
-
config_dict[key] = value
|
|
133
38
|
|
|
134
|
-
|
|
135
|
-
self.save(updated_config)
|
|
136
|
-
except ValidationError as e:
|
|
137
|
-
raise ValueError(
|
|
138
|
-
f"Invalid value set for '{self.__class__.__name__}' with "
|
|
139
|
-
f"key '{key}', value '{value}': '{e}"
|
|
140
|
-
) from e
|
|
141
|
-
except FileNotFoundError as e:
|
|
142
|
-
raise FileNotFoundError(
|
|
143
|
-
f"Resource file for '{self.__class__.__name__}' not found "
|
|
144
|
-
f"at: '{self.path}' when setting key {key}"
|
|
145
|
-
) from e
|
|
146
|
-
|
|
147
|
-
def update(self: Self, updates: dict[str, Any]) -> T:
|
|
148
|
-
"""Updates multiple configuration values at once.
|
|
149
|
-
|
|
150
|
-
Args:
|
|
151
|
-
updates: Dictionary of key-value pairs to update
|
|
152
|
-
|
|
153
|
-
Returns:
|
|
154
|
-
The updated Config object
|
|
155
|
-
|
|
156
|
-
Raises:
|
|
157
|
-
FileNotFoundError: If config file doesn't exist
|
|
158
|
-
ValueError: If the updates config is invalid
|
|
159
|
-
"""
|
|
160
|
-
try:
|
|
161
|
-
config = self.load()
|
|
162
|
-
config_dict = config.model_dump()
|
|
163
|
-
|
|
164
|
-
flat_updates = {k: v for k, v in updates.items() if "." not in k}
|
|
165
|
-
config_dict.update(flat_updates)
|
|
166
|
-
|
|
167
|
-
for key, value in updates.items():
|
|
168
|
-
if "." in key:
|
|
169
|
-
self._set_dot_notation_key(config_dict, key, value)
|
|
170
|
-
|
|
171
|
-
updated_config = config.model_validate(config_dict)
|
|
172
|
-
self.save(updated_config)
|
|
173
|
-
except ValidationError as e:
|
|
174
|
-
raise ValueError(
|
|
175
|
-
f"Invalid value set for '{self.__class__.__name__}' with "
|
|
176
|
-
f"updates '{updates}': '{e}"
|
|
177
|
-
) from e
|
|
178
|
-
except FileNotFoundError as e:
|
|
179
|
-
raise FileNotFoundError(
|
|
180
|
-
f"Resource file for '{self.__class__.__name__}' not found "
|
|
181
|
-
f"at: '{self.path}' when setting updates {updates}"
|
|
182
|
-
) from e
|
|
183
|
-
|
|
184
|
-
return updated_config
|
|
185
|
-
|
|
186
|
-
def reset(self: Self) -> T:
|
|
187
|
-
"""Resets the configuration to defaults.
|
|
188
|
-
|
|
189
|
-
Returns:
|
|
190
|
-
The new default config object
|
|
191
|
-
"""
|
|
192
|
-
config = self.model_class.reset()
|
|
193
|
-
self.save(config)
|
|
194
|
-
return config
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
class ProjectConfigManager(ConfigManager[ProjectConfig]):
|
|
198
|
-
"""Manages the .fmu configuration file in a project."""
|
|
199
|
-
|
|
200
|
-
def __init__(self: Self, fmu_dir: ProjectFMUDirectory) -> None:
|
|
201
|
-
"""Initializes the ProjectConfig resource manager."""
|
|
202
|
-
super().__init__(fmu_dir, ProjectConfig)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
class UserConfigManager(ConfigManager[UserConfig]):
|
|
39
|
+
class UserConfigManager(MutablePydanticResourceManager[UserConfig]):
|
|
206
40
|
"""Manages the .fmu configuration file in a user's home directory."""
|
|
207
41
|
|
|
208
42
|
def __init__(self: Self, fmu_dir: UserFMUDirectory) -> None:
|
|
209
43
|
"""Initializes the UserConfig resource manager."""
|
|
210
44
|
super().__init__(fmu_dir, UserConfig)
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def relative_path(self: Self) -> Path:
|
|
48
|
+
"""Returns the relative path to the config file."""
|
|
49
|
+
return Path("config.json")
|
|
@@ -3,23 +3,28 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
from typing import TYPE_CHECKING, Generic, Self, TypeVar
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Generic, Self, TypeVar
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel, ValidationError
|
|
9
9
|
|
|
10
|
+
from fmu.settings.types import ResettableBaseModel
|
|
11
|
+
|
|
10
12
|
if TYPE_CHECKING:
|
|
13
|
+
# Avoid circular dependency for type hint in __init__ only
|
|
11
14
|
from pathlib import Path
|
|
12
15
|
|
|
13
|
-
# Avoid circular dependency for type hint in __init__ only
|
|
14
16
|
from fmu.settings._fmu_dir import FMUDirectoryBase
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
PydanticResource = TypeVar("PydanticResource", bound=BaseModel)
|
|
19
|
+
MutablePydanticResource = TypeVar("MutablePydanticResource", bound=ResettableBaseModel)
|
|
17
20
|
|
|
18
21
|
|
|
19
|
-
class PydanticResourceManager(Generic[
|
|
22
|
+
class PydanticResourceManager(Generic[PydanticResource]):
|
|
20
23
|
"""Base class for managing resources represented by Pydantic models."""
|
|
21
24
|
|
|
22
|
-
def __init__(
|
|
25
|
+
def __init__(
|
|
26
|
+
self: Self, fmu_dir: FMUDirectoryBase, model_class: type[PydanticResource]
|
|
27
|
+
) -> None:
|
|
23
28
|
"""Initializes the resource manager.
|
|
24
29
|
|
|
25
30
|
Args:
|
|
@@ -28,7 +33,7 @@ class PydanticResourceManager(Generic[T]):
|
|
|
28
33
|
"""
|
|
29
34
|
self.fmu_dir = fmu_dir
|
|
30
35
|
self.model_class = model_class
|
|
31
|
-
self._cache:
|
|
36
|
+
self._cache: PydanticResource | None = None
|
|
32
37
|
|
|
33
38
|
@property
|
|
34
39
|
def relative_path(self: Self) -> Path:
|
|
@@ -48,7 +53,9 @@ class PydanticResourceManager(Generic[T]):
|
|
|
48
53
|
"""Returns whether or not the resource exists."""
|
|
49
54
|
return self.path.exists()
|
|
50
55
|
|
|
51
|
-
def load(
|
|
56
|
+
def load(
|
|
57
|
+
self: Self, force: bool = False, store_cache: bool = True
|
|
58
|
+
) -> PydanticResource:
|
|
52
59
|
"""Loads the resource from disk and validates it as a Pydantic model.
|
|
53
60
|
|
|
54
61
|
Args:
|
|
@@ -92,7 +99,7 @@ class PydanticResourceManager(Generic[T]):
|
|
|
92
99
|
|
|
93
100
|
return self._cache
|
|
94
101
|
|
|
95
|
-
def save(self: Self, model:
|
|
102
|
+
def save(self: Self, model: PydanticResource) -> None:
|
|
96
103
|
"""Save the Pydantic model to disk.
|
|
97
104
|
|
|
98
105
|
Args:
|
|
@@ -102,3 +109,167 @@ class PydanticResourceManager(Generic[T]):
|
|
|
102
109
|
json_data = model.model_dump_json(by_alias=True, indent=2)
|
|
103
110
|
self.fmu_dir.write_text_file(self.relative_path, json_data)
|
|
104
111
|
self._cache = model
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class MutablePydanticResourceManager(PydanticResourceManager[MutablePydanticResource]):
|
|
115
|
+
"""Manages the .fmu configuration file."""
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self: Self, fmu_dir: FMUDirectoryBase, config: type[MutablePydanticResource]
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Initializes the Config resource manager."""
|
|
121
|
+
super().__init__(fmu_dir, config)
|
|
122
|
+
|
|
123
|
+
def _get_dot_notation_key(
|
|
124
|
+
self: Self, config_dict: dict[str, Any], key: str, default: Any = None
|
|
125
|
+
) -> Any:
|
|
126
|
+
"""Sets the value to a dot-notation key.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
config_dict: The configuration dictionary we are modifying (by reference)
|
|
130
|
+
key: The key to set
|
|
131
|
+
default: Value to return if key is not found. Default None
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
The value or default
|
|
135
|
+
"""
|
|
136
|
+
parts = key.split(".")
|
|
137
|
+
value = config_dict
|
|
138
|
+
for part in parts:
|
|
139
|
+
if isinstance(value, dict) and part in value:
|
|
140
|
+
value = value[part]
|
|
141
|
+
else:
|
|
142
|
+
return default
|
|
143
|
+
|
|
144
|
+
return value
|
|
145
|
+
|
|
146
|
+
def get(self: Self, key: str, default: Any = None) -> Any:
|
|
147
|
+
"""Gets a configuration value by key.
|
|
148
|
+
|
|
149
|
+
Supports dot notation for nested values (e.g., "foo.bar")
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
key: The configuration key
|
|
153
|
+
default: Value to return if key is not found. Default None
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
The configuration value or default
|
|
157
|
+
"""
|
|
158
|
+
try:
|
|
159
|
+
config = self.load()
|
|
160
|
+
|
|
161
|
+
if "." in key:
|
|
162
|
+
return self._get_dot_notation_key(config.model_dump(), key, default)
|
|
163
|
+
|
|
164
|
+
if hasattr(config, key):
|
|
165
|
+
return getattr(config, key)
|
|
166
|
+
|
|
167
|
+
config_dict = config.model_dump()
|
|
168
|
+
return config_dict.get(key, default)
|
|
169
|
+
except FileNotFoundError as e:
|
|
170
|
+
raise FileNotFoundError(
|
|
171
|
+
f"Resource file for '{self.__class__.__name__}' not found "
|
|
172
|
+
f"at: '{self.path}' when getting key {key}"
|
|
173
|
+
) from e
|
|
174
|
+
|
|
175
|
+
def _set_dot_notation_key(
|
|
176
|
+
self: Self, config_dict: dict[str, Any], key: str, value: Any
|
|
177
|
+
) -> None:
|
|
178
|
+
"""Sets the value to a dot-notation key.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
config_dict: The configuration dictionary we are modifying (by reference)
|
|
182
|
+
key: The key to set
|
|
183
|
+
value: The value to set
|
|
184
|
+
"""
|
|
185
|
+
parts = key.split(".")
|
|
186
|
+
target = config_dict
|
|
187
|
+
|
|
188
|
+
for part in parts[:-1]:
|
|
189
|
+
if part not in target or not isinstance(target[part], dict):
|
|
190
|
+
target[part] = {}
|
|
191
|
+
target = target[part]
|
|
192
|
+
|
|
193
|
+
target[parts[-1]] = value
|
|
194
|
+
|
|
195
|
+
def set(self: Self, key: str, value: Any) -> None:
|
|
196
|
+
"""Sets a configuration value by key.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
key: The configuration key
|
|
200
|
+
value: The value to set
|
|
201
|
+
|
|
202
|
+
Raises:
|
|
203
|
+
FileNotFoundError: If config file doesn't exist
|
|
204
|
+
ValueError: If the updated config is invalid
|
|
205
|
+
"""
|
|
206
|
+
try:
|
|
207
|
+
config = self.load()
|
|
208
|
+
config_dict = config.model_dump()
|
|
209
|
+
|
|
210
|
+
if "." in key:
|
|
211
|
+
self._set_dot_notation_key(config_dict, key, value)
|
|
212
|
+
else:
|
|
213
|
+
config_dict[key] = value
|
|
214
|
+
|
|
215
|
+
updated_config = config.model_validate(config_dict)
|
|
216
|
+
self.save(updated_config)
|
|
217
|
+
except ValidationError as e:
|
|
218
|
+
raise ValueError(
|
|
219
|
+
f"Invalid value set for '{self.__class__.__name__}' with "
|
|
220
|
+
f"key '{key}', value '{value}': '{e}"
|
|
221
|
+
) from e
|
|
222
|
+
except FileNotFoundError as e:
|
|
223
|
+
raise FileNotFoundError(
|
|
224
|
+
f"Resource file for '{self.__class__.__name__}' not found "
|
|
225
|
+
f"at: '{self.path}' when setting key {key}"
|
|
226
|
+
) from e
|
|
227
|
+
|
|
228
|
+
def update(self: Self, updates: dict[str, Any]) -> MutablePydanticResource:
|
|
229
|
+
"""Updates multiple configuration values at once.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
updates: Dictionary of key-value pairs to update
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
The updated Config object
|
|
236
|
+
|
|
237
|
+
Raises:
|
|
238
|
+
FileNotFoundError: If config file doesn't exist
|
|
239
|
+
ValueError: If the updates config is invalid
|
|
240
|
+
"""
|
|
241
|
+
try:
|
|
242
|
+
config = self.load()
|
|
243
|
+
config_dict = config.model_dump()
|
|
244
|
+
|
|
245
|
+
flat_updates = {k: v for k, v in updates.items() if "." not in k}
|
|
246
|
+
config_dict.update(flat_updates)
|
|
247
|
+
|
|
248
|
+
for key, value in updates.items():
|
|
249
|
+
if "." in key:
|
|
250
|
+
self._set_dot_notation_key(config_dict, key, value)
|
|
251
|
+
|
|
252
|
+
updated_config = config.model_validate(config_dict)
|
|
253
|
+
self.save(updated_config)
|
|
254
|
+
except ValidationError as e:
|
|
255
|
+
raise ValueError(
|
|
256
|
+
f"Invalid value set for '{self.__class__.__name__}' with "
|
|
257
|
+
f"updates '{updates}': '{e}"
|
|
258
|
+
) from e
|
|
259
|
+
except FileNotFoundError as e:
|
|
260
|
+
raise FileNotFoundError(
|
|
261
|
+
f"Resource file for '{self.__class__.__name__}' not found "
|
|
262
|
+
f"at: '{self.path}' when setting updates {updates}"
|
|
263
|
+
) from e
|
|
264
|
+
|
|
265
|
+
return updated_config
|
|
266
|
+
|
|
267
|
+
def reset(self: Self) -> MutablePydanticResource:
|
|
268
|
+
"""Resets the configuration to defaults.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
The new default config object
|
|
272
|
+
"""
|
|
273
|
+
config = self.model_class.reset()
|
|
274
|
+
self.save(config)
|
|
275
|
+
return config
|
fmu/settings/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.5.1'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 5, 1)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
fmu/__init__.py,sha256=htx6HlMme77I6pZ8U256-2B2cMJuELsu3JN3YM2Efh4,144
|
|
2
2
|
fmu/settings/__init__.py,sha256=CkEE7al_uBCQO1lxBKN5LzyCwzzH5Aq6kkEIR7f-zTw,336
|
|
3
3
|
fmu/settings/_fmu_dir.py,sha256=XeZjec78q0IUOpBq-VMkKoWtzXwBeQi2qWRIh_SIFwU,10859
|
|
4
|
-
fmu/settings/_global_config.py,sha256=
|
|
4
|
+
fmu/settings/_global_config.py,sha256=C0_o99OhOc49ynz4h6ygbbHHH8OOI5lcVFr-9FCwD0c,9331
|
|
5
5
|
fmu/settings/_init.py,sha256=ucueS0BlEsM3MkX7IaRISloH4vF7-_ZKSphrORbHgJ4,4381
|
|
6
6
|
fmu/settings/_logging.py,sha256=nEdmZlNCBsB1GfDmFMKCjZmeuRp3CRlbz1EYUemc95Y,1104
|
|
7
|
-
fmu/settings/_version.py,sha256=
|
|
7
|
+
fmu/settings/_version.py,sha256=cYMOhuaBHd0MIZmumuccsEQ-AxM8LIJy9dsBAWgOpqE,704
|
|
8
8
|
fmu/settings/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
fmu/settings/types.py,sha256=aeXEsznBTT1YRRY_LSRqK1j2gmMmyLYYTGYl3a9fweU,513
|
|
10
10
|
fmu/settings/_resources/__init__.py,sha256=LHYR_F7lNGdv8N6R3cEwds5CJQpkOthXFqsEs24vgF8,118
|
|
11
|
-
fmu/settings/_resources/config_managers.py,sha256=
|
|
11
|
+
fmu/settings/_resources/config_managers.py,sha256=QcCLlSw8KdJKrkhGax5teFJzjgQG3ym7Ljs1DykjFbc,1570
|
|
12
12
|
fmu/settings/_resources/lock_manager.py,sha256=zdv1BZJlgB1BO9NepAdjY-YZ1-57HEJcTApE4UVS-8M,9995
|
|
13
|
-
fmu/settings/_resources/pydantic_resource_manager.py,sha256=
|
|
13
|
+
fmu/settings/_resources/pydantic_resource_manager.py,sha256=AVvBUPnYOzmFYo9k5cA9QUme6ZOu0Q3IoLx_le7Mq20,9264
|
|
14
14
|
fmu/settings/models/__init__.py,sha256=lRlXgl55ba2upmDzdvzx8N30JMq2Osnm8aa_xxTZn8A,112
|
|
15
15
|
fmu/settings/models/_enums.py,sha256=SQUZ-2mQcTx4F0oefPFfuQzMKsKTSFSB-wq_CH7TBRE,734
|
|
16
16
|
fmu/settings/models/_mappings.py,sha256=Z4Ex7MtmajBr6FjaNzmwDRwtJlaZZ8YKh9NDmZHRKPI,2832
|
|
17
17
|
fmu/settings/models/lock_info.py,sha256=-oHDF9v9bDLCoFvEg4S6XXYLeo19zRAZ8HynCv75VWg,711
|
|
18
18
|
fmu/settings/models/project_config.py,sha256=pxb54JmpXNMVAFUu_yJ89dNrYEk6hrPuFfFUpf84Jh0,1099
|
|
19
19
|
fmu/settings/models/user_config.py,sha256=dWFTcZY6UnEgNTuGqB-izraJ657PecsW0e0Nt9GBDhI,2666
|
|
20
|
-
fmu_settings-0.
|
|
21
|
-
fmu_settings-0.
|
|
22
|
-
fmu_settings-0.
|
|
23
|
-
fmu_settings-0.
|
|
24
|
-
fmu_settings-0.
|
|
20
|
+
fmu_settings-0.5.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
21
|
+
fmu_settings-0.5.1.dist-info/METADATA,sha256=kVJutk6xJAJ2bHHn-VgKuhoIMtu761lgvC6G5LAr4kM,2116
|
|
22
|
+
fmu_settings-0.5.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
23
|
+
fmu_settings-0.5.1.dist-info/top_level.txt,sha256=Z-FIY3pxn0UK2Wxi9IJ7fKoLSraaxuNGi1eokiE0ShM,4
|
|
24
|
+
fmu_settings-0.5.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|