fmu-settings 0.4.0__tar.gz → 0.5.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.
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.0}/PKG-INFO +1 -1
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/_global_config.py +43 -14
- fmu_settings-0.5.0/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.0/src/fmu/settings/_resources/pydantic_resource_manager.py +107 -42
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/_version.py +3 -3
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu_settings.egg-info/PKG-INFO +1 -1
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/tests/test_global_config.py +28 -14
- fmu_settings-0.4.0/src/fmu/settings/_resources/pydantic_resource_manager.py +0 -104
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/.coveragerc +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/.github/pull_request_template.md +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/.github/workflows/ci.yml +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/.github/workflows/codeql.yml +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/.github/workflows/publish.yml +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/.gitignore +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/CONTRIBUTING.md +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/LICENSE +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/README.md +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/SECURITY.md +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/pyproject.toml +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/setup.cfg +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/__init__.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/__init__.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/_fmu_dir.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/_init.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/_logging.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/_resources/__init__.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/_resources/lock_manager.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/models/__init__.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/models/_enums.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/models/_mappings.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/models/lock_info.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/models/project_config.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/models/user_config.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/py.typed +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/types.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu_settings.egg-info/SOURCES.txt +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu_settings.egg-info/dependency_links.txt +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu_settings.egg-info/requires.txt +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu_settings.egg-info/top_level.txt +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/tests/conftest.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/tests/test_fmu_dir.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/tests/test_init.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/tests/test_resources/test_lock_manager.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/tests/test_resources/test_project_config.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/tests/test_resources/test_resource_managers.py +0 -0
- {fmu_settings-0.4.0 → fmu_settings-0.5.0}/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
|
|
|
@@ -128,8 +152,13 @@ def load_global_configuration_if_present(
|
|
|
128
152
|
global_variables_dict = yaml_load(path, loader=loader)
|
|
129
153
|
global_config = GlobalConfiguration.model_validate(global_variables_dict)
|
|
130
154
|
logger.debug(f"Global variables at {path} has valid settings data")
|
|
131
|
-
except
|
|
132
|
-
logger.debug(f"Global variables at {path}
|
|
155
|
+
except ValidationError as e:
|
|
156
|
+
logger.debug(f"Global variables at {path} failed validation: {e}")
|
|
157
|
+
return None
|
|
158
|
+
except Exception as e:
|
|
159
|
+
logger.debug(
|
|
160
|
+
f"Failed to load global variables at {path}: {type(e).__name__}: {e}"
|
|
161
|
+
)
|
|
133
162
|
return None
|
|
134
163
|
return global_config
|
|
135
164
|
|
|
@@ -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.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 5, 0)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'g53ff72079'
|
|
@@ -15,6 +15,7 @@ from fmu.datamodels.fmu_results.global_configuration import (
|
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
from fmu.settings._global_config import (
|
|
18
|
+
InvalidGlobalConfigurationError,
|
|
18
19
|
_find_global_config_file,
|
|
19
20
|
_find_global_variables_file,
|
|
20
21
|
find_global_config,
|
|
@@ -60,7 +61,9 @@ def test_validate_global_config_strict_model(
|
|
|
60
61
|
if valid:
|
|
61
62
|
validate_global_configuration_strictly(cfg) # Does not raise
|
|
62
63
|
else:
|
|
63
|
-
with pytest.raises(
|
|
64
|
+
with pytest.raises(
|
|
65
|
+
InvalidGlobalConfigurationError, match=f"Invalid name in 'model': {name}"
|
|
66
|
+
):
|
|
64
67
|
validate_global_configuration_strictly(cfg)
|
|
65
68
|
|
|
66
69
|
|
|
@@ -77,7 +80,10 @@ def test_validate_global_config_strict_access(
|
|
|
77
80
|
if valid:
|
|
78
81
|
validate_global_configuration_strictly(cfg) # Does not raise
|
|
79
82
|
else:
|
|
80
|
-
with pytest.raises(
|
|
83
|
+
with pytest.raises(
|
|
84
|
+
InvalidGlobalConfigurationError,
|
|
85
|
+
match=f"Invalid name in 'access.asset': {name}",
|
|
86
|
+
):
|
|
81
87
|
validate_global_configuration_strictly(cfg)
|
|
82
88
|
|
|
83
89
|
|
|
@@ -98,7 +104,8 @@ def test_validate_global_config_strict_smda_country_uuid(
|
|
|
98
104
|
validate_global_configuration_strictly(cfg) # Does not raise
|
|
99
105
|
else:
|
|
100
106
|
with pytest.raises(
|
|
101
|
-
|
|
107
|
+
InvalidGlobalConfigurationError,
|
|
108
|
+
match=f"Invalid SMDA UUID in 'smda.country': {uuid}",
|
|
102
109
|
):
|
|
103
110
|
validate_global_configuration_strictly(cfg)
|
|
104
111
|
|
|
@@ -120,7 +127,7 @@ def test_validate_global_config_strict_smda_discovery_identifier(
|
|
|
120
127
|
validate_global_configuration_strictly(cfg) # Does not raise
|
|
121
128
|
else:
|
|
122
129
|
with pytest.raises(
|
|
123
|
-
|
|
130
|
+
InvalidGlobalConfigurationError,
|
|
124
131
|
match=f"Invalid SMDA short identifier in 'smda.discovery': {identifier}",
|
|
125
132
|
):
|
|
126
133
|
validate_global_configuration_strictly(cfg)
|
|
@@ -143,7 +150,7 @@ def test_validate_global_config_strict_smda_discovery_uuid(
|
|
|
143
150
|
validate_global_configuration_strictly(cfg) # Does not raise
|
|
144
151
|
else:
|
|
145
152
|
with pytest.raises(
|
|
146
|
-
|
|
153
|
+
InvalidGlobalConfigurationError,
|
|
147
154
|
match=f"Invalid SMDA UUID in 'smda.discovery': {uuid}",
|
|
148
155
|
):
|
|
149
156
|
validate_global_configuration_strictly(cfg)
|
|
@@ -166,7 +173,7 @@ def test_validate_global_config_strict_smda_field_identifier(
|
|
|
166
173
|
validate_global_configuration_strictly(cfg) # Does not raise
|
|
167
174
|
else:
|
|
168
175
|
with pytest.raises(
|
|
169
|
-
|
|
176
|
+
InvalidGlobalConfigurationError,
|
|
170
177
|
match=f"Invalid SMDA identifier in 'smda.field': {identifier}",
|
|
171
178
|
):
|
|
172
179
|
validate_global_configuration_strictly(cfg)
|
|
@@ -189,7 +196,7 @@ def test_validate_global_config_strict_smda_field_uuid(
|
|
|
189
196
|
validate_global_configuration_strictly(cfg) # Does not raise
|
|
190
197
|
else:
|
|
191
198
|
with pytest.raises(
|
|
192
|
-
|
|
199
|
+
InvalidGlobalConfigurationError,
|
|
193
200
|
match=f"Invalid SMDA UUID in 'smda.field': {uuid}",
|
|
194
201
|
):
|
|
195
202
|
validate_global_configuration_strictly(cfg)
|
|
@@ -209,7 +216,8 @@ def test_validate_global_config_strict_coordinate_system(
|
|
|
209
216
|
validate_global_configuration_strictly(cfg) # Does not raise
|
|
210
217
|
else:
|
|
211
218
|
with pytest.raises(
|
|
212
|
-
|
|
219
|
+
InvalidGlobalConfigurationError,
|
|
220
|
+
match=f"Invalid SMDA UUID in 'smda.coordinate_system': {uuid}",
|
|
213
221
|
):
|
|
214
222
|
validate_global_configuration_strictly(cfg)
|
|
215
223
|
|
|
@@ -228,7 +236,7 @@ def test_validate_global_config_strict_stratigraphic_column_uuids(
|
|
|
228
236
|
validate_global_configuration_strictly(cfg) # Does not raise
|
|
229
237
|
else:
|
|
230
238
|
with pytest.raises(
|
|
231
|
-
|
|
239
|
+
InvalidGlobalConfigurationError,
|
|
232
240
|
match=f"Invalid SMDA UUID in 'smda.stratigraphic_column': {uuid}",
|
|
233
241
|
):
|
|
234
242
|
validate_global_configuration_strictly(cfg)
|
|
@@ -250,7 +258,7 @@ def test_validate_global_config_strict_stratigraphic_column_names(
|
|
|
250
258
|
validate_global_configuration_strictly(cfg) # Does not raise
|
|
251
259
|
else:
|
|
252
260
|
with pytest.raises(
|
|
253
|
-
|
|
261
|
+
InvalidGlobalConfigurationError,
|
|
254
262
|
match=f"Invalid SMDA identifier in 'smda.stratigraphic_column': "
|
|
255
263
|
f"{identifier}",
|
|
256
264
|
):
|
|
@@ -273,7 +281,7 @@ def test_validate_global_config_strict_stratigraphy_names(
|
|
|
273
281
|
validate_global_configuration_strictly(cfg) # Does not raise
|
|
274
282
|
else:
|
|
275
283
|
with pytest.raises(
|
|
276
|
-
|
|
284
|
+
InvalidGlobalConfigurationError,
|
|
277
285
|
match=f"Invalid stratigraphy name in 'smda.stratigraphy': {identifier}",
|
|
278
286
|
):
|
|
279
287
|
validate_global_configuration_strictly(cfg)
|
|
@@ -420,7 +428,9 @@ def test_find_global_config_from_input_strict(
|
|
|
420
428
|
) -> None:
|
|
421
429
|
"""Tests finding a global config with 'Drogon' in it raises."""
|
|
422
430
|
tmp_path = fmuconfig_with_input
|
|
423
|
-
with pytest.raises(
|
|
431
|
+
with pytest.raises(
|
|
432
|
+
InvalidGlobalConfigurationError, match="Invalid name in 'model': Drogon"
|
|
433
|
+
):
|
|
424
434
|
find_global_config(tmp_path)
|
|
425
435
|
|
|
426
436
|
|
|
@@ -439,7 +449,9 @@ def test_find_global_config_extra_output_paths(
|
|
|
439
449
|
)
|
|
440
450
|
assert isinstance(cfg, GlobalConfiguration)
|
|
441
451
|
|
|
442
|
-
with pytest.raises(
|
|
452
|
+
with pytest.raises(
|
|
453
|
+
InvalidGlobalConfigurationError, match="Invalid name in 'model': Drogon"
|
|
454
|
+
):
|
|
443
455
|
find_global_config(
|
|
444
456
|
base_path,
|
|
445
457
|
extra_output_paths=[tmp_path / "fmuconfig/output/global_variables.yml"],
|
|
@@ -462,7 +474,9 @@ def test_find_global_config_extra_input_paths(
|
|
|
462
474
|
)
|
|
463
475
|
assert isinstance(cfg, GlobalConfiguration)
|
|
464
476
|
|
|
465
|
-
with pytest.raises(
|
|
477
|
+
with pytest.raises(
|
|
478
|
+
InvalidGlobalConfigurationError, match="Invalid name in 'model': Drogon"
|
|
479
|
+
):
|
|
466
480
|
find_global_config(
|
|
467
481
|
base_path,
|
|
468
482
|
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
|