fmu-settings 0.3.2__py3-none-any.whl → 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of fmu-settings might be problematic. Click here for more details.
- fmu/settings/_fmu_dir.py +4 -0
- fmu/settings/_global_config.py +270 -0
- fmu/settings/_init.py +16 -8
- fmu/settings/_resources/config_managers.py +15 -176
- fmu/settings/_resources/lock_manager.py +23 -2
- fmu/settings/_resources/pydantic_resource_manager.py +180 -8
- fmu/settings/_version.py +2 -2
- {fmu_settings-0.3.2.dist-info → fmu_settings-0.5.0.dist-info}/METADATA +4 -1
- {fmu_settings-0.3.2.dist-info → fmu_settings-0.5.0.dist-info}/RECORD +12 -11
- {fmu_settings-0.3.2.dist-info → fmu_settings-0.5.0.dist-info}/WHEEL +0 -0
- {fmu_settings-0.3.2.dist-info → fmu_settings-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {fmu_settings-0.3.2.dist-info → fmu_settings-0.5.0.dist-info}/top_level.txt +0 -0
fmu/settings/_fmu_dir.py
CHANGED
|
@@ -82,7 +82,9 @@ class FMUDirectoryBase:
|
|
|
82
82
|
FileNotFoundError: If config file doesn't exist
|
|
83
83
|
ValueError: If the updated config is invalid
|
|
84
84
|
"""
|
|
85
|
+
logger.info(f"Setting {key} in {self.path}")
|
|
85
86
|
self.config.set(key, value)
|
|
87
|
+
logger.debug(f"Set {key} to {value}")
|
|
86
88
|
|
|
87
89
|
def update_config(
|
|
88
90
|
self: Self, updates: dict[str, Any]
|
|
@@ -147,6 +149,7 @@ class FMUDirectoryBase:
|
|
|
147
149
|
relative_path: Path relative to the .fmu directory
|
|
148
150
|
data: Bytes to write
|
|
149
151
|
"""
|
|
152
|
+
self._lock.ensure_can_write()
|
|
150
153
|
file_path = self.get_file_path(relative_path)
|
|
151
154
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
152
155
|
|
|
@@ -163,6 +166,7 @@ class FMUDirectoryBase:
|
|
|
163
166
|
content: Text content to write
|
|
164
167
|
encoding: Text encoding to use. Default utf-8
|
|
165
168
|
"""
|
|
169
|
+
self._lock.ensure_can_write()
|
|
166
170
|
file_path = self.get_file_path(relative_path)
|
|
167
171
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
168
172
|
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Functions related to finding and validating an existing global configuration."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Final
|
|
5
|
+
|
|
6
|
+
from pydantic import ValidationError
|
|
7
|
+
|
|
8
|
+
from fmu.config.utilities import yaml_load
|
|
9
|
+
from fmu.datamodels.fmu_results.global_configuration import GlobalConfiguration
|
|
10
|
+
|
|
11
|
+
from ._logging import null_logger
|
|
12
|
+
|
|
13
|
+
logger: Final = null_logger(__name__)
|
|
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
|
+
|
|
24
|
+
# These should all be normalized to lower case.
|
|
25
|
+
INVALID_NAMES: Final[tuple[str, ...]] = (
|
|
26
|
+
"drogon",
|
|
27
|
+
"drogon_2020",
|
|
28
|
+
"drogon_has_no_stratcolumn",
|
|
29
|
+
)
|
|
30
|
+
INVALID_UUIDS: Final[tuple[str, ...]] = (
|
|
31
|
+
"ad214d85-dac7-19da-e053-c918a4889309",
|
|
32
|
+
"ad214d85-8a1d-19da-e053-c918a4889310",
|
|
33
|
+
"00000000-0000-0000-0000-000000000000",
|
|
34
|
+
)
|
|
35
|
+
INVALID_STRAT_NAMES: Final[tuple[str, ...]] = (
|
|
36
|
+
"basevolantis",
|
|
37
|
+
"basetherys",
|
|
38
|
+
"basevalysar",
|
|
39
|
+
"basevolon",
|
|
40
|
+
"therys",
|
|
41
|
+
"toptherys",
|
|
42
|
+
"topvolantis",
|
|
43
|
+
"topvolon",
|
|
44
|
+
"topvalysar",
|
|
45
|
+
"valysar",
|
|
46
|
+
"volantis",
|
|
47
|
+
"volon",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def validate_global_configuration_strictly(cfg: GlobalConfiguration) -> None: # noqa: PLR0912
|
|
52
|
+
"""Does stricter checks against a valid GlobalConfiguration file.
|
|
53
|
+
|
|
54
|
+
This is to prevent importing existing but incorrect data that should not make it
|
|
55
|
+
into a project .fmu configuration. An example of this is Drogon masterdata.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
cfg: A GlobalConfiguration instance to be validated
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
InvalidGlobalConfigurationError: If some value in the GlobalConfiguration
|
|
62
|
+
is invalid or not allowed
|
|
63
|
+
"""
|
|
64
|
+
# Check model and access
|
|
65
|
+
if cfg.model.name.lower() in INVALID_NAMES:
|
|
66
|
+
raise InvalidGlobalConfigurationError(
|
|
67
|
+
f"Invalid name in 'model': {cfg.model.name}"
|
|
68
|
+
)
|
|
69
|
+
if cfg.access.asset.name.lower() in INVALID_NAMES:
|
|
70
|
+
raise InvalidGlobalConfigurationError(
|
|
71
|
+
f"Invalid name in 'access.asset': {cfg.access.asset.name}"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Check masterdata
|
|
75
|
+
|
|
76
|
+
# smda.country
|
|
77
|
+
for country in cfg.masterdata.smda.country:
|
|
78
|
+
if str(country.uuid) in INVALID_UUIDS:
|
|
79
|
+
raise InvalidGlobalConfigurationError(
|
|
80
|
+
f"Invalid SMDA UUID in 'smda.country': {country.uuid}"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# smda.discovery
|
|
84
|
+
for discovery in cfg.masterdata.smda.discovery:
|
|
85
|
+
if discovery.short_identifier.lower() in INVALID_NAMES:
|
|
86
|
+
raise InvalidGlobalConfigurationError(
|
|
87
|
+
f"Invalid SMDA short identifier in 'smda.discovery': "
|
|
88
|
+
f"{discovery.short_identifier}"
|
|
89
|
+
)
|
|
90
|
+
if str(discovery.uuid) in INVALID_UUIDS:
|
|
91
|
+
raise InvalidGlobalConfigurationError(
|
|
92
|
+
f"Invalid SMDA UUID in 'smda.discovery': {discovery.uuid}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# smda.field
|
|
96
|
+
for field in cfg.masterdata.smda.field:
|
|
97
|
+
if field.identifier.lower() in INVALID_NAMES:
|
|
98
|
+
raise InvalidGlobalConfigurationError(
|
|
99
|
+
f"Invalid SMDA identifier in 'smda.field': {field.identifier}"
|
|
100
|
+
)
|
|
101
|
+
if str(field.uuid) in INVALID_UUIDS:
|
|
102
|
+
raise InvalidGlobalConfigurationError(
|
|
103
|
+
f"Invalid SMDA UUID in 'smda.field': {field.uuid}"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# smda.coordinate_system
|
|
107
|
+
if (coord_uuid := str(cfg.masterdata.smda.coordinate_system.uuid)) in INVALID_UUIDS:
|
|
108
|
+
raise InvalidGlobalConfigurationError(
|
|
109
|
+
f"Invalid SMDA UUID in 'smda.coordinate_system': {coord_uuid}"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# smda.stratigraphic_column
|
|
113
|
+
strat = cfg.masterdata.smda.stratigraphic_column
|
|
114
|
+
if strat.identifier.lower() in INVALID_NAMES:
|
|
115
|
+
raise InvalidGlobalConfigurationError(
|
|
116
|
+
f"Invalid SMDA identifier in 'smda.stratigraphic_column': "
|
|
117
|
+
f"{strat.identifier}"
|
|
118
|
+
)
|
|
119
|
+
if str(strat.uuid) in INVALID_UUIDS:
|
|
120
|
+
raise InvalidGlobalConfigurationError(
|
|
121
|
+
f"Invalid SMDA UUID in 'smda.stratigraphic_column': {strat.uuid}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Check stratigraphy
|
|
125
|
+
|
|
126
|
+
if cfg.stratigraphy:
|
|
127
|
+
for key in cfg.stratigraphy:
|
|
128
|
+
if key.lower() in INVALID_STRAT_NAMES:
|
|
129
|
+
raise InvalidGlobalConfigurationError(
|
|
130
|
+
f"Invalid stratigraphy name in 'smda.stratigraphy': {key}"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def load_global_configuration_if_present(
|
|
135
|
+
path: Path, fmu_load: bool = False
|
|
136
|
+
) -> GlobalConfiguration | None:
|
|
137
|
+
"""Loads a global config/global variables at a path.
|
|
138
|
+
|
|
139
|
+
This loads via fmu-config, which is capable of loading a global _config_, which is
|
|
140
|
+
different from the global _variables_ in that it may still be in separate files
|
|
141
|
+
linked by the custom '!include' directive.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
path: The path to the yaml file
|
|
145
|
+
fmu_load: Whether or not to load in the custom 'fmu' format. Default False.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
GlobalConfiguration instance or None.
|
|
149
|
+
"""
|
|
150
|
+
loader = "fmu" if fmu_load else "standard"
|
|
151
|
+
try:
|
|
152
|
+
global_variables_dict = yaml_load(path, loader=loader)
|
|
153
|
+
global_config = GlobalConfiguration.model_validate(global_variables_dict)
|
|
154
|
+
logger.debug(f"Global variables at {path} has 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
|
+
)
|
|
162
|
+
return None
|
|
163
|
+
return global_config
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _find_global_variables_file(paths: list[Path]) -> GlobalConfiguration | None:
|
|
167
|
+
"""Finds a valid global variables file, or not.
|
|
168
|
+
|
|
169
|
+
This is the _output_ file after fmuconfig is run.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
paths: A list of Paths to check.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
A validated GlobalConfiguration or None.
|
|
176
|
+
"""
|
|
177
|
+
for path in paths:
|
|
178
|
+
if not path.exists():
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
global_variables_path = path
|
|
182
|
+
# If the path is a dir, and doesn't contain the right file, move on.
|
|
183
|
+
if path.is_dir():
|
|
184
|
+
global_variables_path = path / "global_variables.yml"
|
|
185
|
+
if not global_variables_path.exists():
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
logger.info(f"Found global variables at {path}")
|
|
189
|
+
global_config = load_global_configuration_if_present(global_variables_path)
|
|
190
|
+
if not global_config:
|
|
191
|
+
continue
|
|
192
|
+
return global_config
|
|
193
|
+
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _find_global_config_file(paths: list[Path]) -> GlobalConfiguration | None:
|
|
198
|
+
"""Finds a valid global configuration file, or not.
|
|
199
|
+
|
|
200
|
+
This is the _input_ file, before fmuconfig is run.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
paths: A list of Paths to check.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
A validated GlobalConfiguration or None.
|
|
207
|
+
"""
|
|
208
|
+
for path in paths:
|
|
209
|
+
if not path.exists():
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
logger.info(f"Found global config at {path}")
|
|
213
|
+
# May be global_config*.yml or global_master*.yml
|
|
214
|
+
for global_config_path in path.glob("**/global*.yml"):
|
|
215
|
+
global_config = load_global_configuration_if_present(
|
|
216
|
+
global_config_path, fmu_load=True
|
|
217
|
+
)
|
|
218
|
+
if not global_config:
|
|
219
|
+
continue
|
|
220
|
+
return global_config
|
|
221
|
+
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def find_global_config(
|
|
226
|
+
base_path: str | Path,
|
|
227
|
+
extra_output_paths: list[Path] | None = None,
|
|
228
|
+
extra_input_dirs: list[Path] | None = None,
|
|
229
|
+
strict: bool = True,
|
|
230
|
+
) -> GlobalConfiguration | None:
|
|
231
|
+
"""Try to locate a global configuration with valid masterdata in known location.
|
|
232
|
+
|
|
233
|
+
Extra paths may be provided
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
base_path: The path to the project root
|
|
237
|
+
extra_output_paths: A list of extra paths to a global _variables_.
|
|
238
|
+
extra_input_dirs: A list of extra dirs to a global _config_ might be.
|
|
239
|
+
strict: If True, valid data but invalid _content_ is disallowed, i.e. Drogon
|
|
240
|
+
data. Default True.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
A valid GlobalConfiguration instance, or None.
|
|
244
|
+
"""
|
|
245
|
+
base_path = Path(base_path)
|
|
246
|
+
|
|
247
|
+
# Loads with 'fmu_load=False'
|
|
248
|
+
known_output_paths = [base_path / "fmuconfig/output/global_variables.yml"]
|
|
249
|
+
if extra_output_paths:
|
|
250
|
+
known_output_paths += extra_output_paths
|
|
251
|
+
|
|
252
|
+
global_config = _find_global_variables_file(known_output_paths)
|
|
253
|
+
if global_config:
|
|
254
|
+
if strict:
|
|
255
|
+
validate_global_configuration_strictly(global_config)
|
|
256
|
+
return global_config
|
|
257
|
+
|
|
258
|
+
# Loads with 'fmu_load=True'
|
|
259
|
+
known_input_paths = [base_path / "fmuconfig/input"]
|
|
260
|
+
if extra_input_dirs:
|
|
261
|
+
known_input_paths += extra_input_dirs
|
|
262
|
+
|
|
263
|
+
global_config = _find_global_config_file(known_input_paths)
|
|
264
|
+
if global_config:
|
|
265
|
+
if strict:
|
|
266
|
+
validate_global_configuration_strictly(global_config)
|
|
267
|
+
return global_config
|
|
268
|
+
|
|
269
|
+
logger.info("No global variables or config with valid settings data found.")
|
|
270
|
+
return None
|
fmu/settings/_init.py
CHANGED
|
@@ -4,6 +4,8 @@ from pathlib import Path
|
|
|
4
4
|
from textwrap import dedent
|
|
5
5
|
from typing import Any, Final
|
|
6
6
|
|
|
7
|
+
from fmu.datamodels.fmu_results.global_configuration import GlobalConfiguration
|
|
8
|
+
|
|
7
9
|
from ._fmu_dir import ProjectFMUDirectory, UserFMUDirectory
|
|
8
10
|
from ._logging import null_logger
|
|
9
11
|
from .models.project_config import ProjectConfig
|
|
@@ -66,7 +68,9 @@ def _create_fmu_directory(base_path: Path) -> None:
|
|
|
66
68
|
|
|
67
69
|
|
|
68
70
|
def init_fmu_directory(
|
|
69
|
-
base_path: str | Path,
|
|
71
|
+
base_path: str | Path,
|
|
72
|
+
config_data: ProjectConfig | dict[str, Any] | None = None,
|
|
73
|
+
global_config: GlobalConfiguration | None = None,
|
|
70
74
|
) -> ProjectFMUDirectory:
|
|
71
75
|
"""Creates and initializes a .fmu directory.
|
|
72
76
|
|
|
@@ -74,9 +78,11 @@ def init_fmu_directory(
|
|
|
74
78
|
function.
|
|
75
79
|
|
|
76
80
|
Args:
|
|
77
|
-
base_path: Directory where .fmu should be created
|
|
81
|
+
base_path: Directory where .fmu should be created.
|
|
78
82
|
config_data: Optional ProjectConfig instance or dictionary with configuration
|
|
79
|
-
data
|
|
83
|
+
data.
|
|
84
|
+
global_config: Optional GlobaConfiguration instance with existing global config
|
|
85
|
+
data.
|
|
80
86
|
|
|
81
87
|
Returns:
|
|
82
88
|
Instance of FMUDirectory
|
|
@@ -98,12 +104,14 @@ def init_fmu_directory(
|
|
|
98
104
|
fmu_dir.config.reset()
|
|
99
105
|
if config_data:
|
|
100
106
|
if isinstance(config_data, ProjectConfig):
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
elif isinstance(config_data, dict):
|
|
104
|
-
fmu_dir.update_config(config_data)
|
|
107
|
+
config_data = config_data.model_dump()
|
|
108
|
+
fmu_dir.update_config(config_data)
|
|
105
109
|
|
|
106
|
-
|
|
110
|
+
if global_config:
|
|
111
|
+
for key, value in global_config.model_dump().items():
|
|
112
|
+
fmu_dir.set_config_value(key, value)
|
|
113
|
+
|
|
114
|
+
logger.info(f"Successfully initialized .fmu directory at '{fmu_dir}'")
|
|
107
115
|
return fmu_dir
|
|
108
116
|
|
|
109
117
|
|
|
@@ -3,208 +3,47 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import TYPE_CHECKING,
|
|
7
|
-
|
|
8
|
-
from pydantic import ValidationError
|
|
6
|
+
from typing import TYPE_CHECKING, Final, Self
|
|
9
7
|
|
|
10
8
|
from fmu.settings._logging import null_logger
|
|
11
9
|
from fmu.settings.models.project_config import ProjectConfig
|
|
12
10
|
from fmu.settings.models.user_config import UserConfig
|
|
13
|
-
from fmu.settings.types import ResettableBaseModel # noqa: TC001
|
|
14
11
|
|
|
15
|
-
from .pydantic_resource_manager import
|
|
12
|
+
from .pydantic_resource_manager import (
|
|
13
|
+
MutablePydanticResourceManager,
|
|
14
|
+
)
|
|
16
15
|
|
|
17
16
|
if TYPE_CHECKING:
|
|
18
17
|
# Avoid circular dependency for type hint in __init__ only
|
|
19
18
|
from fmu.settings._fmu_dir import (
|
|
20
|
-
FMUDirectoryBase,
|
|
21
19
|
ProjectFMUDirectory,
|
|
22
20
|
UserFMUDirectory,
|
|
23
21
|
)
|
|
24
22
|
|
|
25
23
|
logger: Final = null_logger(__name__)
|
|
26
24
|
|
|
27
|
-
T = TypeVar("T", bound=ResettableBaseModel)
|
|
28
|
-
|
|
29
25
|
|
|
30
|
-
class
|
|
31
|
-
"""Manages the .fmu configuration file."""
|
|
26
|
+
class ProjectConfigManager(MutablePydanticResourceManager[ProjectConfig]):
|
|
27
|
+
"""Manages the .fmu configuration file in a project."""
|
|
32
28
|
|
|
33
|
-
def __init__(self: Self, fmu_dir:
|
|
34
|
-
"""Initializes the
|
|
35
|
-
super().__init__(fmu_dir,
|
|
29
|
+
def __init__(self: Self, fmu_dir: ProjectFMUDirectory) -> None:
|
|
30
|
+
"""Initializes the ProjectConfig resource manager."""
|
|
31
|
+
super().__init__(fmu_dir, ProjectConfig)
|
|
36
32
|
|
|
37
33
|
@property
|
|
38
34
|
def relative_path(self: Self) -> Path:
|
|
39
35
|
"""Returns the relative path to the config file."""
|
|
40
36
|
return Path("config.json")
|
|
41
37
|
|
|
42
|
-
def _get_dot_notation_key(
|
|
43
|
-
self: Self, config_dict: dict[str, Any], key: str, default: Any = None
|
|
44
|
-
) -> Any:
|
|
45
|
-
"""Sets the value to a dot-notation key.
|
|
46
|
-
|
|
47
|
-
Args:
|
|
48
|
-
config_dict: The configuration dictionary we are modifying (by reference)
|
|
49
|
-
key: The key to set
|
|
50
|
-
default: Value to return if key is not found. Default None
|
|
51
|
-
|
|
52
|
-
Returns:
|
|
53
|
-
The value or default
|
|
54
|
-
"""
|
|
55
|
-
parts = key.split(".")
|
|
56
|
-
value = config_dict
|
|
57
|
-
for part in parts:
|
|
58
|
-
if isinstance(value, dict) and part in value:
|
|
59
|
-
value = value[part]
|
|
60
|
-
else:
|
|
61
|
-
return default
|
|
62
|
-
|
|
63
|
-
return value
|
|
64
|
-
|
|
65
|
-
def get(self: Self, key: str, default: Any = None) -> Any:
|
|
66
|
-
"""Gets a configuration value by key.
|
|
67
|
-
|
|
68
|
-
Supports dot notation for nested values (e.g., "foo.bar")
|
|
69
|
-
|
|
70
|
-
Args:
|
|
71
|
-
key: The configuration key
|
|
72
|
-
default: Value to return if key is not found. Default None
|
|
73
|
-
|
|
74
|
-
Returns:
|
|
75
|
-
The configuration value or default
|
|
76
|
-
"""
|
|
77
|
-
try:
|
|
78
|
-
config = self.load()
|
|
79
|
-
|
|
80
|
-
if "." in key:
|
|
81
|
-
return self._get_dot_notation_key(config.model_dump(), key, default)
|
|
82
|
-
|
|
83
|
-
if hasattr(config, key):
|
|
84
|
-
return getattr(config, key)
|
|
85
|
-
|
|
86
|
-
config_dict = config.model_dump()
|
|
87
|
-
return config_dict.get(key, default)
|
|
88
|
-
except FileNotFoundError as e:
|
|
89
|
-
raise FileNotFoundError(
|
|
90
|
-
f"Resource file for '{self.__class__.__name__}' not found "
|
|
91
|
-
f"at: '{self.path}' when getting key {key}"
|
|
92
|
-
) from e
|
|
93
|
-
|
|
94
|
-
def _set_dot_notation_key(
|
|
95
|
-
self: Self, config_dict: dict[str, Any], key: str, value: Any
|
|
96
|
-
) -> None:
|
|
97
|
-
"""Sets the value to a dot-notation key.
|
|
98
|
-
|
|
99
|
-
Args:
|
|
100
|
-
config_dict: The configuration dictionary we are modifying (by reference)
|
|
101
|
-
key: The key to set
|
|
102
|
-
value: The value to set
|
|
103
|
-
"""
|
|
104
|
-
parts = key.split(".")
|
|
105
|
-
target = config_dict
|
|
106
|
-
|
|
107
|
-
for part in parts[:-1]:
|
|
108
|
-
if part not in target or not isinstance(target[part], dict):
|
|
109
|
-
target[part] = {}
|
|
110
|
-
target = target[part]
|
|
111
|
-
|
|
112
|
-
target[parts[-1]] = value
|
|
113
|
-
|
|
114
|
-
def set(self: Self, key: str, value: Any) -> None:
|
|
115
|
-
"""Sets a configuration value by key.
|
|
116
|
-
|
|
117
|
-
Args:
|
|
118
|
-
key: The configuration key
|
|
119
|
-
value: The value to set
|
|
120
|
-
|
|
121
|
-
Raises:
|
|
122
|
-
FileNotFoundError: If config file doesn't exist
|
|
123
|
-
ValueError: If the updated config is invalid
|
|
124
|
-
"""
|
|
125
|
-
try:
|
|
126
|
-
config = self.load()
|
|
127
|
-
config_dict = config.model_dump()
|
|
128
|
-
|
|
129
|
-
if "." in key:
|
|
130
|
-
self._set_dot_notation_key(config_dict, key, value)
|
|
131
|
-
else:
|
|
132
|
-
config_dict[key] = value
|
|
133
38
|
|
|
134
|
-
|
|
135
|
-
self.save(updated_config)
|
|
136
|
-
except ValidationError as e:
|
|
137
|
-
raise ValueError(
|
|
138
|
-
f"Invalid value set for '{self.__class__.__name__}' with "
|
|
139
|
-
f"key '{key}', value '{value}': '{e}"
|
|
140
|
-
) from e
|
|
141
|
-
except FileNotFoundError as e:
|
|
142
|
-
raise FileNotFoundError(
|
|
143
|
-
f"Resource file for '{self.__class__.__name__}' not found "
|
|
144
|
-
f"at: '{self.path}' when setting key {key}"
|
|
145
|
-
) from e
|
|
146
|
-
|
|
147
|
-
def update(self: Self, updates: dict[str, Any]) -> T:
|
|
148
|
-
"""Updates multiple configuration values at once.
|
|
149
|
-
|
|
150
|
-
Args:
|
|
151
|
-
updates: Dictionary of key-value pairs to update
|
|
152
|
-
|
|
153
|
-
Returns:
|
|
154
|
-
The updated Config object
|
|
155
|
-
|
|
156
|
-
Raises:
|
|
157
|
-
FileNotFoundError: If config file doesn't exist
|
|
158
|
-
ValueError: If the updates config is invalid
|
|
159
|
-
"""
|
|
160
|
-
try:
|
|
161
|
-
config = self.load()
|
|
162
|
-
config_dict = config.model_dump()
|
|
163
|
-
|
|
164
|
-
flat_updates = {k: v for k, v in updates.items() if "." not in k}
|
|
165
|
-
config_dict.update(flat_updates)
|
|
166
|
-
|
|
167
|
-
for key, value in updates.items():
|
|
168
|
-
if "." in key:
|
|
169
|
-
self._set_dot_notation_key(config_dict, key, value)
|
|
170
|
-
|
|
171
|
-
updated_config = config.model_validate(config_dict)
|
|
172
|
-
self.save(updated_config)
|
|
173
|
-
except ValidationError as e:
|
|
174
|
-
raise ValueError(
|
|
175
|
-
f"Invalid value set for '{self.__class__.__name__}' with "
|
|
176
|
-
f"updates '{updates}': '{e}"
|
|
177
|
-
) from e
|
|
178
|
-
except FileNotFoundError as e:
|
|
179
|
-
raise FileNotFoundError(
|
|
180
|
-
f"Resource file for '{self.__class__.__name__}' not found "
|
|
181
|
-
f"at: '{self.path}' when setting updates {updates}"
|
|
182
|
-
) from e
|
|
183
|
-
|
|
184
|
-
return updated_config
|
|
185
|
-
|
|
186
|
-
def reset(self: Self) -> T:
|
|
187
|
-
"""Resets the configuration to defaults.
|
|
188
|
-
|
|
189
|
-
Returns:
|
|
190
|
-
The new default config object
|
|
191
|
-
"""
|
|
192
|
-
config = self.model_class.reset()
|
|
193
|
-
self.save(config)
|
|
194
|
-
return config
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
class ProjectConfigManager(ConfigManager[ProjectConfig]):
|
|
198
|
-
"""Manages the .fmu configuration file in a project."""
|
|
199
|
-
|
|
200
|
-
def __init__(self: Self, fmu_dir: ProjectFMUDirectory) -> None:
|
|
201
|
-
"""Initializes the ProjectConfig resource manager."""
|
|
202
|
-
super().__init__(fmu_dir, ProjectConfig)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
class UserConfigManager(ConfigManager[UserConfig]):
|
|
39
|
+
class UserConfigManager(MutablePydanticResourceManager[UserConfig]):
|
|
206
40
|
"""Manages the .fmu configuration file in a user's home directory."""
|
|
207
41
|
|
|
208
42
|
def __init__(self: Self, fmu_dir: UserFMUDirectory) -> None:
|
|
209
43
|
"""Initializes the UserConfig resource manager."""
|
|
210
44
|
super().__init__(fmu_dir, UserConfig)
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def relative_path(self: Self) -> Path:
|
|
48
|
+
"""Returns the relative path to the config file."""
|
|
49
|
+
return Path("config.json")
|
|
@@ -168,6 +168,25 @@ class LockManager(PydanticResourceManager[LockInfo]):
|
|
|
168
168
|
return False
|
|
169
169
|
return self._is_mine(self._cache) and not self._is_stale()
|
|
170
170
|
|
|
171
|
+
def ensure_can_write(self: Self) -> None:
|
|
172
|
+
"""Raise PermissionError if another process currently holds the lock."""
|
|
173
|
+
try:
|
|
174
|
+
lock_info = self.load(force=True, store_cache=False)
|
|
175
|
+
except Exception:
|
|
176
|
+
lock_info = None
|
|
177
|
+
|
|
178
|
+
if (
|
|
179
|
+
self.exists
|
|
180
|
+
and lock_info is not None
|
|
181
|
+
and not self.is_acquired()
|
|
182
|
+
and not self._is_stale(lock_info=lock_info)
|
|
183
|
+
):
|
|
184
|
+
raise PermissionError(
|
|
185
|
+
"Cannot write to .fmu directory because it is locked by "
|
|
186
|
+
f"{lock_info.user}@{lock_info.hostname} (PID: {lock_info.pid}). "
|
|
187
|
+
f"Lock expires at {time.ctime(lock_info.expires_at)}."
|
|
188
|
+
)
|
|
189
|
+
|
|
171
190
|
def refresh(self: Self) -> None:
|
|
172
191
|
"""Refresh/extend the lock expiration time.
|
|
173
192
|
|
|
@@ -239,9 +258,11 @@ class LockManager(PydanticResourceManager[LockInfo]):
|
|
|
239
258
|
except Exception:
|
|
240
259
|
return None
|
|
241
260
|
|
|
242
|
-
def _is_stale(self: Self) -> bool:
|
|
261
|
+
def _is_stale(self: Self, lock_info: LockInfo | None = None) -> bool:
|
|
243
262
|
"""Check if existing lock is stale (expired or process dead)."""
|
|
244
|
-
lock_info
|
|
263
|
+
if lock_info is None:
|
|
264
|
+
lock_info = self._safe_load()
|
|
265
|
+
|
|
245
266
|
if not lock_info:
|
|
246
267
|
return True
|
|
247
268
|
|
|
@@ -3,23 +3,28 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
from typing import TYPE_CHECKING, Generic, Self, TypeVar
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Generic, Self, TypeVar
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel, ValidationError
|
|
9
9
|
|
|
10
|
+
from fmu.settings.types import ResettableBaseModel
|
|
11
|
+
|
|
10
12
|
if TYPE_CHECKING:
|
|
13
|
+
# Avoid circular dependency for type hint in __init__ only
|
|
11
14
|
from pathlib import Path
|
|
12
15
|
|
|
13
|
-
# Avoid circular dependency for type hint in __init__ only
|
|
14
16
|
from fmu.settings._fmu_dir import FMUDirectoryBase
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
PydanticResource = TypeVar("PydanticResource", bound=BaseModel)
|
|
19
|
+
MutablePydanticResource = TypeVar("MutablePydanticResource", bound=ResettableBaseModel)
|
|
17
20
|
|
|
18
21
|
|
|
19
|
-
class PydanticResourceManager(Generic[
|
|
22
|
+
class PydanticResourceManager(Generic[PydanticResource]):
|
|
20
23
|
"""Base class for managing resources represented by Pydantic models."""
|
|
21
24
|
|
|
22
|
-
def __init__(
|
|
25
|
+
def __init__(
|
|
26
|
+
self: Self, fmu_dir: FMUDirectoryBase, model_class: type[PydanticResource]
|
|
27
|
+
) -> None:
|
|
23
28
|
"""Initializes the resource manager.
|
|
24
29
|
|
|
25
30
|
Args:
|
|
@@ -28,7 +33,7 @@ class PydanticResourceManager(Generic[T]):
|
|
|
28
33
|
"""
|
|
29
34
|
self.fmu_dir = fmu_dir
|
|
30
35
|
self.model_class = model_class
|
|
31
|
-
self._cache:
|
|
36
|
+
self._cache: PydanticResource | None = None
|
|
32
37
|
|
|
33
38
|
@property
|
|
34
39
|
def relative_path(self: Self) -> Path:
|
|
@@ -48,7 +53,9 @@ class PydanticResourceManager(Generic[T]):
|
|
|
48
53
|
"""Returns whether or not the resource exists."""
|
|
49
54
|
return self.path.exists()
|
|
50
55
|
|
|
51
|
-
def load(
|
|
56
|
+
def load(
|
|
57
|
+
self: Self, force: bool = False, store_cache: bool = True
|
|
58
|
+
) -> PydanticResource:
|
|
52
59
|
"""Loads the resource from disk and validates it as a Pydantic model.
|
|
53
60
|
|
|
54
61
|
Args:
|
|
@@ -92,12 +99,177 @@ class PydanticResourceManager(Generic[T]):
|
|
|
92
99
|
|
|
93
100
|
return self._cache
|
|
94
101
|
|
|
95
|
-
def save(self: Self, model:
|
|
102
|
+
def save(self: Self, model: PydanticResource) -> None:
|
|
96
103
|
"""Save the Pydantic model to disk.
|
|
97
104
|
|
|
98
105
|
Args:
|
|
99
106
|
model: Validated Pydantic model instance
|
|
100
107
|
"""
|
|
108
|
+
self.fmu_dir._lock.ensure_can_write()
|
|
101
109
|
json_data = model.model_dump_json(by_alias=True, indent=2)
|
|
102
110
|
self.fmu_dir.write_text_file(self.relative_path, json_data)
|
|
103
111
|
self._cache = model
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class MutablePydanticResourceManager(PydanticResourceManager[MutablePydanticResource]):
|
|
115
|
+
"""Manages the .fmu configuration file."""
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self: Self, fmu_dir: FMUDirectoryBase, config: type[MutablePydanticResource]
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Initializes the Config resource manager."""
|
|
121
|
+
super().__init__(fmu_dir, config)
|
|
122
|
+
|
|
123
|
+
def _get_dot_notation_key(
|
|
124
|
+
self: Self, config_dict: dict[str, Any], key: str, default: Any = None
|
|
125
|
+
) -> Any:
|
|
126
|
+
"""Sets the value to a dot-notation key.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
config_dict: The configuration dictionary we are modifying (by reference)
|
|
130
|
+
key: The key to set
|
|
131
|
+
default: Value to return if key is not found. Default None
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
The value or default
|
|
135
|
+
"""
|
|
136
|
+
parts = key.split(".")
|
|
137
|
+
value = config_dict
|
|
138
|
+
for part in parts:
|
|
139
|
+
if isinstance(value, dict) and part in value:
|
|
140
|
+
value = value[part]
|
|
141
|
+
else:
|
|
142
|
+
return default
|
|
143
|
+
|
|
144
|
+
return value
|
|
145
|
+
|
|
146
|
+
def get(self: Self, key: str, default: Any = None) -> Any:
|
|
147
|
+
"""Gets a configuration value by key.
|
|
148
|
+
|
|
149
|
+
Supports dot notation for nested values (e.g., "foo.bar")
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
key: The configuration key
|
|
153
|
+
default: Value to return if key is not found. Default None
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
The configuration value or default
|
|
157
|
+
"""
|
|
158
|
+
try:
|
|
159
|
+
config = self.load()
|
|
160
|
+
|
|
161
|
+
if "." in key:
|
|
162
|
+
return self._get_dot_notation_key(config.model_dump(), key, default)
|
|
163
|
+
|
|
164
|
+
if hasattr(config, key):
|
|
165
|
+
return getattr(config, key)
|
|
166
|
+
|
|
167
|
+
config_dict = config.model_dump()
|
|
168
|
+
return config_dict.get(key, default)
|
|
169
|
+
except FileNotFoundError as e:
|
|
170
|
+
raise FileNotFoundError(
|
|
171
|
+
f"Resource file for '{self.__class__.__name__}' not found "
|
|
172
|
+
f"at: '{self.path}' when getting key {key}"
|
|
173
|
+
) from e
|
|
174
|
+
|
|
175
|
+
def _set_dot_notation_key(
|
|
176
|
+
self: Self, config_dict: dict[str, Any], key: str, value: Any
|
|
177
|
+
) -> None:
|
|
178
|
+
"""Sets the value to a dot-notation key.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
config_dict: The configuration dictionary we are modifying (by reference)
|
|
182
|
+
key: The key to set
|
|
183
|
+
value: The value to set
|
|
184
|
+
"""
|
|
185
|
+
parts = key.split(".")
|
|
186
|
+
target = config_dict
|
|
187
|
+
|
|
188
|
+
for part in parts[:-1]:
|
|
189
|
+
if part not in target or not isinstance(target[part], dict):
|
|
190
|
+
target[part] = {}
|
|
191
|
+
target = target[part]
|
|
192
|
+
|
|
193
|
+
target[parts[-1]] = value
|
|
194
|
+
|
|
195
|
+
def set(self: Self, key: str, value: Any) -> None:
|
|
196
|
+
"""Sets a configuration value by key.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
key: The configuration key
|
|
200
|
+
value: The value to set
|
|
201
|
+
|
|
202
|
+
Raises:
|
|
203
|
+
FileNotFoundError: If config file doesn't exist
|
|
204
|
+
ValueError: If the updated config is invalid
|
|
205
|
+
"""
|
|
206
|
+
try:
|
|
207
|
+
config = self.load()
|
|
208
|
+
config_dict = config.model_dump()
|
|
209
|
+
|
|
210
|
+
if "." in key:
|
|
211
|
+
self._set_dot_notation_key(config_dict, key, value)
|
|
212
|
+
else:
|
|
213
|
+
config_dict[key] = value
|
|
214
|
+
|
|
215
|
+
updated_config = config.model_validate(config_dict)
|
|
216
|
+
self.save(updated_config)
|
|
217
|
+
except ValidationError as e:
|
|
218
|
+
raise ValueError(
|
|
219
|
+
f"Invalid value set for '{self.__class__.__name__}' with "
|
|
220
|
+
f"key '{key}', value '{value}': '{e}"
|
|
221
|
+
) from e
|
|
222
|
+
except FileNotFoundError as e:
|
|
223
|
+
raise FileNotFoundError(
|
|
224
|
+
f"Resource file for '{self.__class__.__name__}' not found "
|
|
225
|
+
f"at: '{self.path}' when setting key {key}"
|
|
226
|
+
) from e
|
|
227
|
+
|
|
228
|
+
def update(self: Self, updates: dict[str, Any]) -> MutablePydanticResource:
|
|
229
|
+
"""Updates multiple configuration values at once.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
updates: Dictionary of key-value pairs to update
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
The updated Config object
|
|
236
|
+
|
|
237
|
+
Raises:
|
|
238
|
+
FileNotFoundError: If config file doesn't exist
|
|
239
|
+
ValueError: If the updates config is invalid
|
|
240
|
+
"""
|
|
241
|
+
try:
|
|
242
|
+
config = self.load()
|
|
243
|
+
config_dict = config.model_dump()
|
|
244
|
+
|
|
245
|
+
flat_updates = {k: v for k, v in updates.items() if "." not in k}
|
|
246
|
+
config_dict.update(flat_updates)
|
|
247
|
+
|
|
248
|
+
for key, value in updates.items():
|
|
249
|
+
if "." in key:
|
|
250
|
+
self._set_dot_notation_key(config_dict, key, value)
|
|
251
|
+
|
|
252
|
+
updated_config = config.model_validate(config_dict)
|
|
253
|
+
self.save(updated_config)
|
|
254
|
+
except ValidationError as e:
|
|
255
|
+
raise ValueError(
|
|
256
|
+
f"Invalid value set for '{self.__class__.__name__}' with "
|
|
257
|
+
f"updates '{updates}': '{e}"
|
|
258
|
+
) from e
|
|
259
|
+
except FileNotFoundError as e:
|
|
260
|
+
raise FileNotFoundError(
|
|
261
|
+
f"Resource file for '{self.__class__.__name__}' not found "
|
|
262
|
+
f"at: '{self.path}' when setting updates {updates}"
|
|
263
|
+
) from e
|
|
264
|
+
|
|
265
|
+
return updated_config
|
|
266
|
+
|
|
267
|
+
def reset(self: Self) -> MutablePydanticResource:
|
|
268
|
+
"""Resets the configuration to defaults.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
The new default config object
|
|
272
|
+
"""
|
|
273
|
+
config = self.model_class.reset()
|
|
274
|
+
self.save(config)
|
|
275
|
+
return config
|
fmu/settings/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.5.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 5, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fmu-settings
|
|
3
|
-
Version: 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
|
|
@@ -17,7 +17,9 @@ Classifier: Natural Language :: English
|
|
|
17
17
|
Requires-Python: >=3.11
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
License-File: LICENSE
|
|
20
|
+
Requires-Dist: PyYAML
|
|
20
21
|
Requires-Dist: annotated_types
|
|
22
|
+
Requires-Dist: fmu-config
|
|
21
23
|
Requires-Dist: fmu-datamodels
|
|
22
24
|
Requires-Dist: pydantic
|
|
23
25
|
Provides-Extra: dev
|
|
@@ -27,6 +29,7 @@ Requires-Dist: pytest-cov; extra == "dev"
|
|
|
27
29
|
Requires-Dist: pytest-mock; extra == "dev"
|
|
28
30
|
Requires-Dist: pytest-xdist; extra == "dev"
|
|
29
31
|
Requires-Dist: ruff; extra == "dev"
|
|
32
|
+
Requires-Dist: types-PyYAML; extra == "dev"
|
|
30
33
|
Dynamic: license-file
|
|
31
34
|
|
|
32
35
|
# fmu-settings
|
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
fmu/__init__.py,sha256=htx6HlMme77I6pZ8U256-2B2cMJuELsu3JN3YM2Efh4,144
|
|
2
2
|
fmu/settings/__init__.py,sha256=CkEE7al_uBCQO1lxBKN5LzyCwzzH5Aq6kkEIR7f-zTw,336
|
|
3
|
-
fmu/settings/_fmu_dir.py,sha256=
|
|
4
|
-
fmu/settings/
|
|
3
|
+
fmu/settings/_fmu_dir.py,sha256=XeZjec78q0IUOpBq-VMkKoWtzXwBeQi2qWRIh_SIFwU,10859
|
|
4
|
+
fmu/settings/_global_config.py,sha256=tYSQH_48cknaEeo8C_uraEaPXZrrN7cHYZOCX9G39yM,8916
|
|
5
|
+
fmu/settings/_init.py,sha256=ucueS0BlEsM3MkX7IaRISloH4vF7-_ZKSphrORbHgJ4,4381
|
|
5
6
|
fmu/settings/_logging.py,sha256=nEdmZlNCBsB1GfDmFMKCjZmeuRp3CRlbz1EYUemc95Y,1104
|
|
6
|
-
fmu/settings/_version.py,sha256=
|
|
7
|
+
fmu/settings/_version.py,sha256=fvHpBU3KZKRinkriKdtAt3crenOyysELF-M9y3ozg3U,704
|
|
7
8
|
fmu/settings/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
9
|
fmu/settings/types.py,sha256=aeXEsznBTT1YRRY_LSRqK1j2gmMmyLYYTGYl3a9fweU,513
|
|
9
10
|
fmu/settings/_resources/__init__.py,sha256=LHYR_F7lNGdv8N6R3cEwds5CJQpkOthXFqsEs24vgF8,118
|
|
10
|
-
fmu/settings/_resources/config_managers.py,sha256=
|
|
11
|
-
fmu/settings/_resources/lock_manager.py,sha256=
|
|
12
|
-
fmu/settings/_resources/pydantic_resource_manager.py,sha256=
|
|
11
|
+
fmu/settings/_resources/config_managers.py,sha256=QcCLlSw8KdJKrkhGax5teFJzjgQG3ym7Ljs1DykjFbc,1570
|
|
12
|
+
fmu/settings/_resources/lock_manager.py,sha256=zdv1BZJlgB1BO9NepAdjY-YZ1-57HEJcTApE4UVS-8M,9995
|
|
13
|
+
fmu/settings/_resources/pydantic_resource_manager.py,sha256=AVvBUPnYOzmFYo9k5cA9QUme6ZOu0Q3IoLx_le7Mq20,9264
|
|
13
14
|
fmu/settings/models/__init__.py,sha256=lRlXgl55ba2upmDzdvzx8N30JMq2Osnm8aa_xxTZn8A,112
|
|
14
15
|
fmu/settings/models/_enums.py,sha256=SQUZ-2mQcTx4F0oefPFfuQzMKsKTSFSB-wq_CH7TBRE,734
|
|
15
16
|
fmu/settings/models/_mappings.py,sha256=Z4Ex7MtmajBr6FjaNzmwDRwtJlaZZ8YKh9NDmZHRKPI,2832
|
|
16
17
|
fmu/settings/models/lock_info.py,sha256=-oHDF9v9bDLCoFvEg4S6XXYLeo19zRAZ8HynCv75VWg,711
|
|
17
18
|
fmu/settings/models/project_config.py,sha256=pxb54JmpXNMVAFUu_yJ89dNrYEk6hrPuFfFUpf84Jh0,1099
|
|
18
19
|
fmu/settings/models/user_config.py,sha256=dWFTcZY6UnEgNTuGqB-izraJ657PecsW0e0Nt9GBDhI,2666
|
|
19
|
-
fmu_settings-0.
|
|
20
|
-
fmu_settings-0.
|
|
21
|
-
fmu_settings-0.
|
|
22
|
-
fmu_settings-0.
|
|
23
|
-
fmu_settings-0.
|
|
20
|
+
fmu_settings-0.5.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
21
|
+
fmu_settings-0.5.0.dist-info/METADATA,sha256=5Ff9Xfr_KsWUptwWt4HlKIkfnmME7D2UqclD-qzgrSo,2116
|
|
22
|
+
fmu_settings-0.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
23
|
+
fmu_settings-0.5.0.dist-info/top_level.txt,sha256=Z-FIY3pxn0UK2Wxi9IJ7fKoLSraaxuNGi1eokiE0ShM,4
|
|
24
|
+
fmu_settings-0.5.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|