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.

Files changed (46) hide show
  1. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/PKG-INFO +1 -1
  2. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/_global_config.py +57 -15
  3. fmu_settings-0.5.1/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.1/src/fmu/settings/_resources/pydantic_resource_manager.py +107 -42
  5. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/_version.py +3 -3
  6. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu_settings.egg-info/PKG-INFO +1 -1
  7. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/tests/test_global_config.py +110 -37
  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.1}/.coveragerc +0 -0
  10. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/.github/pull_request_template.md +0 -0
  11. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/.github/workflows/ci.yml +0 -0
  12. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/.github/workflows/codeql.yml +0 -0
  13. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/.github/workflows/publish.yml +0 -0
  14. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/.gitignore +0 -0
  15. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/CONTRIBUTING.md +0 -0
  16. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/LICENSE +0 -0
  17. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/README.md +0 -0
  18. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/SECURITY.md +0 -0
  19. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/pyproject.toml +0 -0
  20. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/setup.cfg +0 -0
  21. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/__init__.py +0 -0
  22. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/__init__.py +0 -0
  23. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/_fmu_dir.py +0 -0
  24. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/_init.py +0 -0
  25. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/_logging.py +0 -0
  26. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/_resources/__init__.py +0 -0
  27. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/_resources/lock_manager.py +0 -0
  28. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/models/__init__.py +0 -0
  29. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/models/_enums.py +0 -0
  30. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/models/_mappings.py +0 -0
  31. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/models/lock_info.py +0 -0
  32. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/models/project_config.py +0 -0
  33. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/models/user_config.py +0 -0
  34. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/py.typed +0 -0
  35. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu/settings/types.py +0 -0
  36. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu_settings.egg-info/SOURCES.txt +0 -0
  37. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu_settings.egg-info/dependency_links.txt +0 -0
  38. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu_settings.egg-info/requires.txt +0 -0
  39. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/src/fmu_settings.egg-info/top_level.txt +0 -0
  40. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/tests/conftest.py +0 -0
  41. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/tests/test_fmu_dir.py +0 -0
  42. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/tests/test_init.py +0 -0
  43. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/tests/test_resources/test_lock_manager.py +0 -0
  44. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/tests/test_resources/test_project_config.py +0 -0
  45. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/tests/test_resources/test_resource_managers.py +0 -0
  46. {fmu_settings-0.4.0 → fmu_settings-0.5.1}/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.1
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
 
@@ -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 Exception:
132
- logger.debug(f"Global variables at {path} does not have valid settings data")
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
- """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.1'
32
+ __version_tuple__ = version_tuple = (0, 5, 1)
33
33
 
34
- __commit_id__ = commit_id = 'g51366b17b'
34
+ __commit_id__ = commit_id = 'g6786842d9'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fmu-settings
3
- Version: 0.4.0
3
+ Version: 0.5.1
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
@@ -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(ValueError, match=f"Invalid name in 'model': {name}"):
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(ValueError, match=f"Invalid name in 'access.asset': {name}"):
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
- ValueError, match=f"Invalid SMDA UUID in 'smda.country': {uuid}"
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
- ValueError,
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
- ValueError,
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
- ValueError,
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
- ValueError,
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
- ValueError, match=f"Invalid SMDA UUID in 'smda.coordinate_system': {uuid}"
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
- ValueError,
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
- ValueError,
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
- ValueError,
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 test_load_global_configuration_returns_none_if_invalid_yaml(
295
+ def test_load_global_configuration_raises_on_invalid_yaml_structure(
287
296
  fmu_load: bool, tmp_path: Path
288
297
  ) -> None:
289
- """Tests maybe_load returns None if an Exception is raised."""
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
- assert load_global_configuration_if_present(config_path, fmu_load=fmu_load) is None
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 test_load_global_configuration_returns_none_if_invalid_config(
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 maybe_load returns None if an Exception is raised."""
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 test_find_global_configs_file_malformed(tmp_path: Path) -> None:
323
- """Tests finding the global config file if it is malformed."""
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 test_find_global_variables_file_malformed(tmp_path: Path) -> None:
359
- """Tests finding the global variables file if it is malformed."""
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
- assert _find_global_variables_file([tmp_path]) is None
408
+ with pytest.raises(ValidationError):
409
+ _find_global_variables_file([tmp_path])
363
410
 
364
411
 
365
- def test_find_global_variables_file(fmuconfig_with_output: Path) -> None:
366
- """Tests finding the global variables file in fmuconfig."""
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, some_file]) is None
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(ValueError, match="Invalid name in 'model': Drogon"):
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(ValueError, match="Invalid name in 'model': Drogon"):
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(ValueError, match="Invalid name in 'model': Drogon"):
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