fmu-settings 0.4.0__tar.gz → 0.5.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.
Potentially problematic release.
This version of fmu-settings might be problematic. Click here for more details.
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/PKG-INFO +1 -1
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/_global_config.py +57 -15
- fmu_settings-0.5.1/src/fmu/settings/_resources/config_managers.py +49 -0
- fmu_settings-0.4.0/src/fmu/settings/_resources/config_managers.py → fmu_settings-0.5.1/src/fmu/settings/_resources/pydantic_resource_manager.py +107 -42
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/_version.py +3 -3
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu_settings.egg-info/PKG-INFO +1 -1
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/tests/test_global_config.py +110 -37
- fmu_settings-0.4.0/src/fmu/settings/_resources/pydantic_resource_manager.py +0 -104
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/.coveragerc +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/.github/pull_request_template.md +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/.github/workflows/ci.yml +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/.github/workflows/codeql.yml +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/.github/workflows/publish.yml +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/.gitignore +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/CONTRIBUTING.md +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/LICENSE +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/README.md +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/SECURITY.md +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/pyproject.toml +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/setup.cfg +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/__init__.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/__init__.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/_fmu_dir.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/_init.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/_logging.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/_resources/__init__.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/_resources/lock_manager.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/models/__init__.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/models/_enums.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/models/_mappings.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/models/lock_info.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/models/project_config.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/models/user_config.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/py.typed +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/types.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu_settings.egg-info/SOURCES.txt +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu_settings.egg-info/dependency_links.txt +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu_settings.egg-info/requires.txt +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu_settings.egg-info/top_level.txt +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/tests/conftest.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/tests/test_fmu_dir.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/tests/test_init.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/tests/test_resources/test_lock_manager.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/tests/test_resources/test_project_config.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/tests/test_resources/test_resource_managers.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.1}/tests/test_resources/test_user_config.py +0 -0
|
@@ -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
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""The generic configuration file in a .fmu directory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING, Final, Self
|
|
7
|
+
|
|
8
|
+
from fmu.settings._logging import null_logger
|
|
9
|
+
from fmu.settings.models.project_config import ProjectConfig
|
|
10
|
+
from fmu.settings.models.user_config import UserConfig
|
|
11
|
+
|
|
12
|
+
from .pydantic_resource_manager import (
|
|
13
|
+
MutablePydanticResourceManager,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
# Avoid circular dependency for type hint in __init__ only
|
|
18
|
+
from fmu.settings._fmu_dir import (
|
|
19
|
+
ProjectFMUDirectory,
|
|
20
|
+
UserFMUDirectory,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
logger: Final = null_logger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ProjectConfigManager(MutablePydanticResourceManager[ProjectConfig]):
|
|
27
|
+
"""Manages the .fmu configuration file in a project."""
|
|
28
|
+
|
|
29
|
+
def __init__(self: Self, fmu_dir: ProjectFMUDirectory) -> None:
|
|
30
|
+
"""Initializes the ProjectConfig resource manager."""
|
|
31
|
+
super().__init__(fmu_dir, ProjectConfig)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def relative_path(self: Self) -> Path:
|
|
35
|
+
"""Returns the relative path to the config file."""
|
|
36
|
+
return Path("config.json")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class UserConfigManager(MutablePydanticResourceManager[UserConfig]):
|
|
40
|
+
"""Manages the .fmu configuration file in a user's home directory."""
|
|
41
|
+
|
|
42
|
+
def __init__(self: Self, fmu_dir: UserFMUDirectory) -> None:
|
|
43
|
+
"""Initializes the UserConfig resource manager."""
|
|
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")
|
|
@@ -1,43 +1,124 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Contains the base class used for interacting with resources."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
from typing import TYPE_CHECKING, Any,
|
|
5
|
+
import json
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Generic, Self, TypeVar
|
|
7
7
|
|
|
8
|
-
from pydantic import ValidationError
|
|
8
|
+
from pydantic import BaseModel, ValidationError
|
|
9
9
|
|
|
10
|
-
from fmu.settings.
|
|
11
|
-
from fmu.settings.models.project_config import ProjectConfig
|
|
12
|
-
from fmu.settings.models.user_config import UserConfig
|
|
13
|
-
from fmu.settings.types import ResettableBaseModel # noqa: TC001
|
|
14
|
-
|
|
15
|
-
from .pydantic_resource_manager import PydanticResourceManager
|
|
10
|
+
from fmu.settings.types import ResettableBaseModel
|
|
16
11
|
|
|
17
12
|
if TYPE_CHECKING:
|
|
18
13
|
# Avoid circular dependency for type hint in __init__ only
|
|
19
|
-
from
|
|
20
|
-
FMUDirectoryBase,
|
|
21
|
-
ProjectFMUDirectory,
|
|
22
|
-
UserFMUDirectory,
|
|
23
|
-
)
|
|
14
|
+
from pathlib import Path
|
|
24
15
|
|
|
25
|
-
|
|
16
|
+
from fmu.settings._fmu_dir import FMUDirectoryBase
|
|
26
17
|
|
|
27
|
-
|
|
18
|
+
PydanticResource = TypeVar("PydanticResource", bound=BaseModel)
|
|
19
|
+
MutablePydanticResource = TypeVar("MutablePydanticResource", bound=ResettableBaseModel)
|
|
28
20
|
|
|
29
21
|
|
|
30
|
-
class
|
|
31
|
-
"""
|
|
22
|
+
class PydanticResourceManager(Generic[PydanticResource]):
|
|
23
|
+
"""Base class for managing resources represented by Pydantic models."""
|
|
32
24
|
|
|
33
|
-
def __init__(
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
def __init__(
|
|
26
|
+
self: Self, fmu_dir: FMUDirectoryBase, model_class: type[PydanticResource]
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Initializes the resource manager.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
fmu_dir: The FMUDirectory instance
|
|
32
|
+
model_class: The Pydantic model class this manager handles
|
|
33
|
+
"""
|
|
34
|
+
self.fmu_dir = fmu_dir
|
|
35
|
+
self.model_class = model_class
|
|
36
|
+
self._cache: PydanticResource | None = None
|
|
36
37
|
|
|
37
38
|
@property
|
|
38
39
|
def relative_path(self: Self) -> Path:
|
|
39
|
-
"""Returns the
|
|
40
|
-
|
|
40
|
+
"""Returns the path to the resource file _inside_ the .fmu directory.
|
|
41
|
+
|
|
42
|
+
Must be implemented by subclasses.
|
|
43
|
+
"""
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def path(self: Self) -> Path:
|
|
48
|
+
"""Returns the full path to the resource file."""
|
|
49
|
+
return self.fmu_dir.get_file_path(self.relative_path)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def exists(self: Self) -> bool:
|
|
53
|
+
"""Returns whether or not the resource exists."""
|
|
54
|
+
return self.path.exists()
|
|
55
|
+
|
|
56
|
+
def load(
|
|
57
|
+
self: Self, force: bool = False, store_cache: bool = True
|
|
58
|
+
) -> PydanticResource:
|
|
59
|
+
"""Loads the resource from disk and validates it as a Pydantic model.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
force: Force a re-read even if the file is already cached.
|
|
63
|
+
store_cache: Whether or not to cache the loaded model internally. This is
|
|
64
|
+
best used with 'force=True' because if a model is already stored in
|
|
65
|
+
_cache it will be returned without re-loading. Default True.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Validated Pydantic model
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ValueError: If the resource file is missing or data does not match the
|
|
72
|
+
model schema
|
|
73
|
+
"""
|
|
74
|
+
if self._cache is None or force:
|
|
75
|
+
if not self.exists:
|
|
76
|
+
raise FileNotFoundError(
|
|
77
|
+
f"Resource file for '{self.__class__.__name__}' not found "
|
|
78
|
+
f"at: '{self.path}'"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
content = self.fmu_dir.read_text_file(self.relative_path)
|
|
83
|
+
data = json.loads(content)
|
|
84
|
+
validated_model = self.model_class.model_validate(data)
|
|
85
|
+
if store_cache:
|
|
86
|
+
self._cache = validated_model
|
|
87
|
+
else:
|
|
88
|
+
return validated_model
|
|
89
|
+
except ValidationError as e:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"Invalid content in resource file for '{self.__class__.__name__}: "
|
|
92
|
+
f"'{e}"
|
|
93
|
+
) from e
|
|
94
|
+
except json.JSONDecodeError as e:
|
|
95
|
+
raise ValueError(
|
|
96
|
+
f"Invalid JSON in resource file for '{self.__class__.__name__}': "
|
|
97
|
+
f"'{e}'"
|
|
98
|
+
) from e
|
|
99
|
+
|
|
100
|
+
return self._cache
|
|
101
|
+
|
|
102
|
+
def save(self: Self, model: PydanticResource) -> None:
|
|
103
|
+
"""Save the Pydantic model to disk.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
model: Validated Pydantic model instance
|
|
107
|
+
"""
|
|
108
|
+
self.fmu_dir._lock.ensure_can_write()
|
|
109
|
+
json_data = model.model_dump_json(by_alias=True, indent=2)
|
|
110
|
+
self.fmu_dir.write_text_file(self.relative_path, json_data)
|
|
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)
|
|
41
122
|
|
|
42
123
|
def _get_dot_notation_key(
|
|
43
124
|
self: Self, config_dict: dict[str, Any], key: str, default: Any = None
|
|
@@ -144,7 +225,7 @@ class ConfigManager(PydanticResourceManager[T]):
|
|
|
144
225
|
f"at: '{self.path}' when setting key {key}"
|
|
145
226
|
) from e
|
|
146
227
|
|
|
147
|
-
def update(self: Self, updates: dict[str, Any]) ->
|
|
228
|
+
def update(self: Self, updates: dict[str, Any]) -> MutablePydanticResource:
|
|
148
229
|
"""Updates multiple configuration values at once.
|
|
149
230
|
|
|
150
231
|
Args:
|
|
@@ -183,7 +264,7 @@ class ConfigManager(PydanticResourceManager[T]):
|
|
|
183
264
|
|
|
184
265
|
return updated_config
|
|
185
266
|
|
|
186
|
-
def reset(self: Self) ->
|
|
267
|
+
def reset(self: Self) -> MutablePydanticResource:
|
|
187
268
|
"""Resets the configuration to defaults.
|
|
188
269
|
|
|
189
270
|
Returns:
|
|
@@ -192,19 +273,3 @@ class ConfigManager(PydanticResourceManager[T]):
|
|
|
192
273
|
config = self.model_class.reset()
|
|
193
274
|
self.save(config)
|
|
194
275
|
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]):
|
|
206
|
-
"""Manages the .fmu configuration file in a user's home directory."""
|
|
207
|
-
|
|
208
|
-
def __init__(self: Self, fmu_dir: UserFMUDirectory) -> None:
|
|
209
|
-
"""Initializes the UserConfig resource manager."""
|
|
210
|
-
super().__init__(fmu_dir, UserConfig)
|
|
@@ -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
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'g6786842d9'
|
|
@@ -13,8 +13,10 @@ from fmu.datamodels.fmu_results.global_configuration import (
|
|
|
13
13
|
GlobalConfiguration,
|
|
14
14
|
StratigraphyElement,
|
|
15
15
|
)
|
|
16
|
+
from pydantic import ValidationError
|
|
16
17
|
|
|
17
18
|
from fmu.settings._global_config import (
|
|
19
|
+
InvalidGlobalConfigurationError,
|
|
18
20
|
_find_global_config_file,
|
|
19
21
|
_find_global_variables_file,
|
|
20
22
|
find_global_config,
|
|
@@ -60,7 +62,9 @@ def test_validate_global_config_strict_model(
|
|
|
60
62
|
if valid:
|
|
61
63
|
validate_global_configuration_strictly(cfg) # Does not raise
|
|
62
64
|
else:
|
|
63
|
-
with pytest.raises(
|
|
65
|
+
with pytest.raises(
|
|
66
|
+
InvalidGlobalConfigurationError, match=f"Invalid name in 'model': {name}"
|
|
67
|
+
):
|
|
64
68
|
validate_global_configuration_strictly(cfg)
|
|
65
69
|
|
|
66
70
|
|
|
@@ -77,7 +81,10 @@ def test_validate_global_config_strict_access(
|
|
|
77
81
|
if valid:
|
|
78
82
|
validate_global_configuration_strictly(cfg) # Does not raise
|
|
79
83
|
else:
|
|
80
|
-
with pytest.raises(
|
|
84
|
+
with pytest.raises(
|
|
85
|
+
InvalidGlobalConfigurationError,
|
|
86
|
+
match=f"Invalid name in 'access.asset': {name}",
|
|
87
|
+
):
|
|
81
88
|
validate_global_configuration_strictly(cfg)
|
|
82
89
|
|
|
83
90
|
|
|
@@ -98,7 +105,8 @@ def test_validate_global_config_strict_smda_country_uuid(
|
|
|
98
105
|
validate_global_configuration_strictly(cfg) # Does not raise
|
|
99
106
|
else:
|
|
100
107
|
with pytest.raises(
|
|
101
|
-
|
|
108
|
+
InvalidGlobalConfigurationError,
|
|
109
|
+
match=f"Invalid SMDA UUID in 'smda.country': {uuid}",
|
|
102
110
|
):
|
|
103
111
|
validate_global_configuration_strictly(cfg)
|
|
104
112
|
|
|
@@ -120,7 +128,7 @@ def test_validate_global_config_strict_smda_discovery_identifier(
|
|
|
120
128
|
validate_global_configuration_strictly(cfg) # Does not raise
|
|
121
129
|
else:
|
|
122
130
|
with pytest.raises(
|
|
123
|
-
|
|
131
|
+
InvalidGlobalConfigurationError,
|
|
124
132
|
match=f"Invalid SMDA short identifier in 'smda.discovery': {identifier}",
|
|
125
133
|
):
|
|
126
134
|
validate_global_configuration_strictly(cfg)
|
|
@@ -143,7 +151,7 @@ def test_validate_global_config_strict_smda_discovery_uuid(
|
|
|
143
151
|
validate_global_configuration_strictly(cfg) # Does not raise
|
|
144
152
|
else:
|
|
145
153
|
with pytest.raises(
|
|
146
|
-
|
|
154
|
+
InvalidGlobalConfigurationError,
|
|
147
155
|
match=f"Invalid SMDA UUID in 'smda.discovery': {uuid}",
|
|
148
156
|
):
|
|
149
157
|
validate_global_configuration_strictly(cfg)
|
|
@@ -166,7 +174,7 @@ def test_validate_global_config_strict_smda_field_identifier(
|
|
|
166
174
|
validate_global_configuration_strictly(cfg) # Does not raise
|
|
167
175
|
else:
|
|
168
176
|
with pytest.raises(
|
|
169
|
-
|
|
177
|
+
InvalidGlobalConfigurationError,
|
|
170
178
|
match=f"Invalid SMDA identifier in 'smda.field': {identifier}",
|
|
171
179
|
):
|
|
172
180
|
validate_global_configuration_strictly(cfg)
|
|
@@ -189,7 +197,7 @@ def test_validate_global_config_strict_smda_field_uuid(
|
|
|
189
197
|
validate_global_configuration_strictly(cfg) # Does not raise
|
|
190
198
|
else:
|
|
191
199
|
with pytest.raises(
|
|
192
|
-
|
|
200
|
+
InvalidGlobalConfigurationError,
|
|
193
201
|
match=f"Invalid SMDA UUID in 'smda.field': {uuid}",
|
|
194
202
|
):
|
|
195
203
|
validate_global_configuration_strictly(cfg)
|
|
@@ -209,7 +217,8 @@ def test_validate_global_config_strict_coordinate_system(
|
|
|
209
217
|
validate_global_configuration_strictly(cfg) # Does not raise
|
|
210
218
|
else:
|
|
211
219
|
with pytest.raises(
|
|
212
|
-
|
|
220
|
+
InvalidGlobalConfigurationError,
|
|
221
|
+
match=f"Invalid SMDA UUID in 'smda.coordinate_system': {uuid}",
|
|
213
222
|
):
|
|
214
223
|
validate_global_configuration_strictly(cfg)
|
|
215
224
|
|
|
@@ -228,7 +237,7 @@ def test_validate_global_config_strict_stratigraphic_column_uuids(
|
|
|
228
237
|
validate_global_configuration_strictly(cfg) # Does not raise
|
|
229
238
|
else:
|
|
230
239
|
with pytest.raises(
|
|
231
|
-
|
|
240
|
+
InvalidGlobalConfigurationError,
|
|
232
241
|
match=f"Invalid SMDA UUID in 'smda.stratigraphic_column': {uuid}",
|
|
233
242
|
):
|
|
234
243
|
validate_global_configuration_strictly(cfg)
|
|
@@ -250,7 +259,7 @@ def test_validate_global_config_strict_stratigraphic_column_names(
|
|
|
250
259
|
validate_global_configuration_strictly(cfg) # Does not raise
|
|
251
260
|
else:
|
|
252
261
|
with pytest.raises(
|
|
253
|
-
|
|
262
|
+
InvalidGlobalConfigurationError,
|
|
254
263
|
match=f"Invalid SMDA identifier in 'smda.stratigraphic_column': "
|
|
255
264
|
f"{identifier}",
|
|
256
265
|
):
|
|
@@ -273,7 +282,7 @@ def test_validate_global_config_strict_stratigraphy_names(
|
|
|
273
282
|
validate_global_configuration_strictly(cfg) # Does not raise
|
|
274
283
|
else:
|
|
275
284
|
with pytest.raises(
|
|
276
|
-
|
|
285
|
+
InvalidGlobalConfigurationError,
|
|
277
286
|
match=f"Invalid stratigraphy name in 'smda.stratigraphy': {identifier}",
|
|
278
287
|
):
|
|
279
288
|
validate_global_configuration_strictly(cfg)
|
|
@@ -283,29 +292,51 @@ def test_validate_global_config_strict_stratigraphy_names(
|
|
|
283
292
|
|
|
284
293
|
|
|
285
294
|
@pytest.mark.parametrize("fmu_load", [True, False])
|
|
286
|
-
def
|
|
295
|
+
def test_load_global_configuration_raises_on_invalid_yaml_structure(
|
|
287
296
|
fmu_load: bool, tmp_path: Path
|
|
288
297
|
) -> None:
|
|
289
|
-
"""Tests
|
|
298
|
+
"""Tests that ValidationError is raised for invalid YAML structure."""
|
|
290
299
|
config_path = tmp_path / "global_config.yml"
|
|
291
300
|
with open(config_path, "w") as f:
|
|
292
301
|
f.write("foo=bar")
|
|
293
302
|
|
|
294
|
-
|
|
303
|
+
with pytest.raises(ValidationError):
|
|
304
|
+
load_global_configuration_if_present(config_path, fmu_load=fmu_load)
|
|
295
305
|
|
|
296
306
|
|
|
297
307
|
@pytest.mark.parametrize("fmu_load", [True, False])
|
|
298
|
-
def
|
|
308
|
+
def test_load_global_configuration_raises_on_missing_required_fields(
|
|
299
309
|
fmu_load: bool,
|
|
300
310
|
tmp_path: Path,
|
|
301
311
|
global_variables_with_masterdata: dict[str, Any],
|
|
302
312
|
) -> None:
|
|
303
|
-
"""Tests
|
|
313
|
+
"""Tests that ValidationError is raised for missing required fields."""
|
|
304
314
|
config_path = tmp_path / "global_config.yml"
|
|
305
315
|
del global_variables_with_masterdata["masterdata"]
|
|
306
316
|
with open(config_path, "w") as f:
|
|
307
317
|
yaml.safe_dump(global_variables_with_masterdata, f)
|
|
308
318
|
|
|
319
|
+
with pytest.raises(ValidationError):
|
|
320
|
+
load_global_configuration_if_present(config_path, fmu_load=fmu_load)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@pytest.mark.parametrize("fmu_load", [True, False])
|
|
324
|
+
def test_load_global_configuration_returns_none_on_file_not_found(
|
|
325
|
+
fmu_load: bool, tmp_path: Path
|
|
326
|
+
) -> None:
|
|
327
|
+
"""Tests that None is returned when file doesn't exist."""
|
|
328
|
+
config_path = tmp_path / "non_existent_file.yml"
|
|
329
|
+
assert load_global_configuration_if_present(config_path, fmu_load=fmu_load) is None
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@pytest.mark.parametrize("fmu_load", [True, False])
|
|
333
|
+
def test_load_global_configuration_returns_none_on_yaml_parse_error(
|
|
334
|
+
fmu_load: bool, tmp_path: Path
|
|
335
|
+
) -> None:
|
|
336
|
+
"""Tests that None is returned on YAML parsing errors."""
|
|
337
|
+
config_path = tmp_path / "invalid.yml"
|
|
338
|
+
with open(config_path, "w") as f:
|
|
339
|
+
f.write("key: [unclosed list")
|
|
309
340
|
assert load_global_configuration_if_present(config_path, fmu_load=fmu_load) is None
|
|
310
341
|
|
|
311
342
|
|
|
@@ -319,10 +350,23 @@ def test_find_global_config_file_not_there(tmp_path: Path) -> None:
|
|
|
319
350
|
assert _find_global_config_file([tmp_path / "dne"]) is None
|
|
320
351
|
|
|
321
352
|
|
|
322
|
-
def
|
|
323
|
-
|
|
353
|
+
def test_find_global_config_file_malformed_raises_validation_error(
|
|
354
|
+
tmp_path: Path,
|
|
355
|
+
) -> None:
|
|
356
|
+
"""Tests that malformed global config file raises ValidationError."""
|
|
324
357
|
with open(tmp_path / "global_master_config.yml", "w") as f:
|
|
325
358
|
f.write("foo: bar")
|
|
359
|
+
with pytest.raises(ValidationError):
|
|
360
|
+
_find_global_config_file([tmp_path])
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def test_find_global_config_file_skips_invalid_yaml_and_continues(
|
|
364
|
+
tmp_path: Path,
|
|
365
|
+
) -> None:
|
|
366
|
+
"""Tests that function skips files with YAML parse errors and continues."""
|
|
367
|
+
with open(tmp_path / "global_config.yml", "w") as f:
|
|
368
|
+
f.write("key: [unclosed list")
|
|
369
|
+
|
|
326
370
|
assert _find_global_config_file([tmp_path]) is None
|
|
327
371
|
|
|
328
372
|
|
|
@@ -355,32 +399,55 @@ def test_find_global_variables_file_not_there(tmp_path: Path) -> None:
|
|
|
355
399
|
assert _find_global_variables_file([tmp_path / "dne"]) is None
|
|
356
400
|
|
|
357
401
|
|
|
358
|
-
def
|
|
359
|
-
|
|
402
|
+
def test_find_global_variables_file_malformed_raises_validation_error(
|
|
403
|
+
tmp_path: Path,
|
|
404
|
+
) -> None:
|
|
405
|
+
"""Tests that malformed global variables file raises ValidationError."""
|
|
360
406
|
with open(tmp_path / "global_variables.yml", "w") as f:
|
|
361
407
|
f.write("foo: bar")
|
|
362
|
-
|
|
408
|
+
with pytest.raises(ValidationError):
|
|
409
|
+
_find_global_variables_file([tmp_path])
|
|
363
410
|
|
|
364
411
|
|
|
365
|
-
def
|
|
366
|
-
|
|
412
|
+
def test_find_global_variables_file_returns_none_when_not_found(
|
|
413
|
+
fmuconfig_with_output: Path,
|
|
414
|
+
) -> None:
|
|
415
|
+
"""Tests that None is returned when no global variables file exists."""
|
|
367
416
|
tmp_path = fmuconfig_with_output
|
|
368
417
|
some_dir = tmp_path / "some_dir"
|
|
369
418
|
some_dir.mkdir()
|
|
370
|
-
some_file = some_dir / "some_file"
|
|
371
|
-
some_file.touch()
|
|
372
419
|
does_not_exist = tmp_path / "bad"
|
|
373
|
-
assert _find_global_variables_file([does_not_exist, some_dir
|
|
420
|
+
assert _find_global_variables_file([does_not_exist, some_dir]) is None
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def test_find_global_variables_file_skips_invalid_yaml_and_continues(
|
|
424
|
+
tmp_path: Path,
|
|
425
|
+
) -> None:
|
|
426
|
+
"""Tests that function skips files with YAML parse errors and continues."""
|
|
427
|
+
invalid_yaml_file = tmp_path / "global_variables.yml"
|
|
428
|
+
with open(invalid_yaml_file, "w") as f:
|
|
429
|
+
f.write("key: [unclosed list")
|
|
374
430
|
|
|
431
|
+
assert _find_global_variables_file([tmp_path]) is None
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def test_find_global_variables_file_raises_on_empty_file(
|
|
435
|
+
fmuconfig_with_output: Path,
|
|
436
|
+
) -> None:
|
|
437
|
+
"""Tests that ValidationError is raised for empty/invalid file."""
|
|
438
|
+
tmp_path = fmuconfig_with_output
|
|
439
|
+
some_file = tmp_path / "some_file"
|
|
440
|
+
some_file.touch()
|
|
441
|
+
with pytest.raises(ValidationError):
|
|
442
|
+
_find_global_variables_file([some_file])
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def test_find_global_variables_file_returns_valid_config(
|
|
446
|
+
fmuconfig_with_output: Path,
|
|
447
|
+
) -> None:
|
|
448
|
+
"""Tests finding a valid global variables file in fmuconfig."""
|
|
375
449
|
assert isinstance(
|
|
376
|
-
_find_global_variables_file(
|
|
377
|
-
[
|
|
378
|
-
does_not_exist,
|
|
379
|
-
some_dir,
|
|
380
|
-
some_file,
|
|
381
|
-
fmuconfig_with_output / "fmuconfig/output",
|
|
382
|
-
]
|
|
383
|
-
),
|
|
450
|
+
_find_global_variables_file([fmuconfig_with_output / "fmuconfig/output"]),
|
|
384
451
|
GlobalConfiguration,
|
|
385
452
|
)
|
|
386
453
|
|
|
@@ -420,7 +487,9 @@ def test_find_global_config_from_input_strict(
|
|
|
420
487
|
) -> None:
|
|
421
488
|
"""Tests finding a global config with 'Drogon' in it raises."""
|
|
422
489
|
tmp_path = fmuconfig_with_input
|
|
423
|
-
with pytest.raises(
|
|
490
|
+
with pytest.raises(
|
|
491
|
+
InvalidGlobalConfigurationError, match="Invalid name in 'model': Drogon"
|
|
492
|
+
):
|
|
424
493
|
find_global_config(tmp_path)
|
|
425
494
|
|
|
426
495
|
|
|
@@ -439,7 +508,9 @@ def test_find_global_config_extra_output_paths(
|
|
|
439
508
|
)
|
|
440
509
|
assert isinstance(cfg, GlobalConfiguration)
|
|
441
510
|
|
|
442
|
-
with pytest.raises(
|
|
511
|
+
with pytest.raises(
|
|
512
|
+
InvalidGlobalConfigurationError, match="Invalid name in 'model': Drogon"
|
|
513
|
+
):
|
|
443
514
|
find_global_config(
|
|
444
515
|
base_path,
|
|
445
516
|
extra_output_paths=[tmp_path / "fmuconfig/output/global_variables.yml"],
|
|
@@ -462,7 +533,9 @@ def test_find_global_config_extra_input_paths(
|
|
|
462
533
|
)
|
|
463
534
|
assert isinstance(cfg, GlobalConfiguration)
|
|
464
535
|
|
|
465
|
-
with pytest.raises(
|
|
536
|
+
with pytest.raises(
|
|
537
|
+
InvalidGlobalConfigurationError, match="Invalid name in 'model': Drogon"
|
|
538
|
+
):
|
|
466
539
|
find_global_config(
|
|
467
540
|
base_path,
|
|
468
541
|
extra_input_dirs=[tmp_path / "fmuconfig/input"],
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
"""Contains the base class used for interacting with resources."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
from typing import TYPE_CHECKING, Generic, Self, TypeVar
|
|
7
|
-
|
|
8
|
-
from pydantic import BaseModel, ValidationError
|
|
9
|
-
|
|
10
|
-
if TYPE_CHECKING:
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
|
|
13
|
-
# Avoid circular dependency for type hint in __init__ only
|
|
14
|
-
from fmu.settings._fmu_dir import FMUDirectoryBase
|
|
15
|
-
|
|
16
|
-
T = TypeVar("T", bound=BaseModel)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class PydanticResourceManager(Generic[T]):
|
|
20
|
-
"""Base class for managing resources represented by Pydantic models."""
|
|
21
|
-
|
|
22
|
-
def __init__(self: Self, fmu_dir: FMUDirectoryBase, model_class: type[T]) -> None:
|
|
23
|
-
"""Initializes the resource manager.
|
|
24
|
-
|
|
25
|
-
Args:
|
|
26
|
-
fmu_dir: The FMUDirectory instance
|
|
27
|
-
model_class: The Pydantic model class this manager handles
|
|
28
|
-
"""
|
|
29
|
-
self.fmu_dir = fmu_dir
|
|
30
|
-
self.model_class = model_class
|
|
31
|
-
self._cache: T | None = None
|
|
32
|
-
|
|
33
|
-
@property
|
|
34
|
-
def relative_path(self: Self) -> Path:
|
|
35
|
-
"""Returns the path to the resource file _inside_ the .fmu directory.
|
|
36
|
-
|
|
37
|
-
Must be implemented by subclasses.
|
|
38
|
-
"""
|
|
39
|
-
raise NotImplementedError
|
|
40
|
-
|
|
41
|
-
@property
|
|
42
|
-
def path(self: Self) -> Path:
|
|
43
|
-
"""Returns the full path to the resource file."""
|
|
44
|
-
return self.fmu_dir.get_file_path(self.relative_path)
|
|
45
|
-
|
|
46
|
-
@property
|
|
47
|
-
def exists(self: Self) -> bool:
|
|
48
|
-
"""Returns whether or not the resource exists."""
|
|
49
|
-
return self.path.exists()
|
|
50
|
-
|
|
51
|
-
def load(self: Self, force: bool = False, store_cache: bool = True) -> T:
|
|
52
|
-
"""Loads the resource from disk and validates it as a Pydantic model.
|
|
53
|
-
|
|
54
|
-
Args:
|
|
55
|
-
force: Force a re-read even if the file is already cached.
|
|
56
|
-
store_cache: Whether or not to cache the loaded model internally. This is
|
|
57
|
-
best used with 'force=True' because if a model is already stored in
|
|
58
|
-
_cache it will be returned without re-loading. Default True.
|
|
59
|
-
|
|
60
|
-
Returns:
|
|
61
|
-
Validated Pydantic model
|
|
62
|
-
|
|
63
|
-
Raises:
|
|
64
|
-
ValueError: If the resource file is missing or data does not match the
|
|
65
|
-
model schema
|
|
66
|
-
"""
|
|
67
|
-
if self._cache is None or force:
|
|
68
|
-
if not self.exists:
|
|
69
|
-
raise FileNotFoundError(
|
|
70
|
-
f"Resource file for '{self.__class__.__name__}' not found "
|
|
71
|
-
f"at: '{self.path}'"
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
try:
|
|
75
|
-
content = self.fmu_dir.read_text_file(self.relative_path)
|
|
76
|
-
data = json.loads(content)
|
|
77
|
-
validated_model = self.model_class.model_validate(data)
|
|
78
|
-
if store_cache:
|
|
79
|
-
self._cache = validated_model
|
|
80
|
-
else:
|
|
81
|
-
return validated_model
|
|
82
|
-
except ValidationError as e:
|
|
83
|
-
raise ValueError(
|
|
84
|
-
f"Invalid content in resource file for '{self.__class__.__name__}: "
|
|
85
|
-
f"'{e}"
|
|
86
|
-
) from e
|
|
87
|
-
except json.JSONDecodeError as e:
|
|
88
|
-
raise ValueError(
|
|
89
|
-
f"Invalid JSON in resource file for '{self.__class__.__name__}': "
|
|
90
|
-
f"'{e}'"
|
|
91
|
-
) from e
|
|
92
|
-
|
|
93
|
-
return self._cache
|
|
94
|
-
|
|
95
|
-
def save(self: Self, model: T) -> None:
|
|
96
|
-
"""Save the Pydantic model to disk.
|
|
97
|
-
|
|
98
|
-
Args:
|
|
99
|
-
model: Validated Pydantic model instance
|
|
100
|
-
"""
|
|
101
|
-
self.fmu_dir._lock.ensure_can_write()
|
|
102
|
-
json_data = model.model_dump_json(by_alias=True, indent=2)
|
|
103
|
-
self.fmu_dir.write_text_file(self.relative_path, json_data)
|
|
104
|
-
self._cache = model
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|