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.

Files changed (46) hide show
  1. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/PKG-INFO +1 -1
  2. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/_global_config.py +43 -14
  3. fmu_settings-0.5.0/src/fmu/settings/_resources/config_managers.py +49 -0
  4. 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
  5. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/_version.py +3 -3
  6. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu_settings.egg-info/PKG-INFO +1 -1
  7. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/tests/test_global_config.py +28 -14
  8. fmu_settings-0.4.0/src/fmu/settings/_resources/pydantic_resource_manager.py +0 -104
  9. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/.coveragerc +0 -0
  10. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/.github/pull_request_template.md +0 -0
  11. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/.github/workflows/ci.yml +0 -0
  12. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/.github/workflows/codeql.yml +0 -0
  13. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/.github/workflows/publish.yml +0 -0
  14. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/.gitignore +0 -0
  15. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/CONTRIBUTING.md +0 -0
  16. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/LICENSE +0 -0
  17. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/README.md +0 -0
  18. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/SECURITY.md +0 -0
  19. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/pyproject.toml +0 -0
  20. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/setup.cfg +0 -0
  21. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/__init__.py +0 -0
  22. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/__init__.py +0 -0
  23. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/_fmu_dir.py +0 -0
  24. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/_init.py +0 -0
  25. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/_logging.py +0 -0
  26. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/_resources/__init__.py +0 -0
  27. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/_resources/lock_manager.py +0 -0
  28. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/models/__init__.py +0 -0
  29. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/models/_enums.py +0 -0
  30. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/models/_mappings.py +0 -0
  31. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/models/lock_info.py +0 -0
  32. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/models/project_config.py +0 -0
  33. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/models/user_config.py +0 -0
  34. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/py.typed +0 -0
  35. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu/settings/types.py +0 -0
  36. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu_settings.egg-info/SOURCES.txt +0 -0
  37. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu_settings.egg-info/dependency_links.txt +0 -0
  38. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu_settings.egg-info/requires.txt +0 -0
  39. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/src/fmu_settings.egg-info/top_level.txt +0 -0
  40. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/tests/conftest.py +0 -0
  41. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/tests/test_fmu_dir.py +0 -0
  42. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/tests/test_init.py +0 -0
  43. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/tests/test_resources/test_lock_manager.py +0 -0
  44. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/tests/test_resources/test_project_config.py +0 -0
  45. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/tests/test_resources/test_resource_managers.py +0 -0
  46. {fmu_settings-0.4.0 → fmu_settings-0.5.0}/tests/test_resources/test_user_config.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fmu-settings
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: A library for managing FMU settings
5
5
  Author-email: Equinor <fg-fmu_atlas@equinor.com>
6
6
  License: GPL-3.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
- ValueError: If some value in the GlobalConfiguration is invalid or not allowed
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 ValueError(f"Invalid name in 'model': {cfg.model.name}")
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 ValueError(f"Invalid name in 'access.asset': {cfg.access.asset.name}")
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 ValueError(f"Invalid SMDA UUID in 'smda.country': {country.uuid}")
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 ValueError(
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 ValueError(f"Invalid SMDA UUID in 'smda.discovery': {discovery.uuid}")
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 ValueError(
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 ValueError(f"Invalid SMDA UUID in 'smda.field': {field.uuid}")
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 ValueError(f"Invalid SMDA UUID in 'smda.coordinate_system': {coord_uuid}")
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 ValueError(
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 ValueError(
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 ValueError(
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 Exception:
132
- logger.debug(f"Global variables at {path} does not have valid settings data")
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
- """The generic configuration file in a .fmu directory."""
1
+ """Contains the base class used for interacting with resources."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from pathlib import Path
6
- from typing import TYPE_CHECKING, Any, Final, Self, TypeVar
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._logging import null_logger
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 fmu.settings._fmu_dir import (
20
- FMUDirectoryBase,
21
- ProjectFMUDirectory,
22
- UserFMUDirectory,
23
- )
14
+ from pathlib import Path
24
15
 
25
- logger: Final = null_logger(__name__)
16
+ from fmu.settings._fmu_dir import FMUDirectoryBase
26
17
 
27
- T = TypeVar("T", bound=ResettableBaseModel)
18
+ PydanticResource = TypeVar("PydanticResource", bound=BaseModel)
19
+ MutablePydanticResource = TypeVar("MutablePydanticResource", bound=ResettableBaseModel)
28
20
 
29
21
 
30
- class ConfigManager(PydanticResourceManager[T]):
31
- """Manages the .fmu configuration file."""
22
+ class PydanticResourceManager(Generic[PydanticResource]):
23
+ """Base class for managing resources represented by Pydantic models."""
32
24
 
33
- def __init__(self: Self, fmu_dir: FMUDirectoryBase, config: type[T]) -> None:
34
- """Initializes the Config resource manager."""
35
- super().__init__(fmu_dir, config)
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 relative path to the config file."""
40
- return Path("config.json")
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]) -> T:
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) -> T:
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.4.0'
32
- __version_tuple__ = version_tuple = (0, 4, 0)
31
+ __version__ = version = '0.5.0'
32
+ __version_tuple__ = version_tuple = (0, 5, 0)
33
33
 
34
- __commit_id__ = commit_id = 'g51366b17b'
34
+ __commit_id__ = commit_id = 'g53ff72079'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fmu-settings
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: A library for managing FMU settings
5
5
  Author-email: Equinor <fg-fmu_atlas@equinor.com>
6
6
  License: GPL-3.0
@@ -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(ValueError, match=f"Invalid name in 'model': {name}"):
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(ValueError, match=f"Invalid name in 'access.asset': {name}"):
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
- ValueError, match=f"Invalid SMDA UUID in 'smda.country': {uuid}"
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
- ValueError,
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
- ValueError,
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
- ValueError,
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
- ValueError,
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
- ValueError, match=f"Invalid SMDA UUID in 'smda.coordinate_system': {uuid}"
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
- ValueError,
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
- ValueError,
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
- ValueError,
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(ValueError, match="Invalid name in 'model': Drogon"):
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(ValueError, match="Invalid name in 'model': Drogon"):
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(ValueError, match="Invalid name in 'model': Drogon"):
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