fmu-settings 0.0.1__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/__init__.py +6 -0
- fmu/settings/__init__.py +12 -0
- fmu/settings/_fmu_dir.py +337 -0
- fmu/settings/_init.py +131 -0
- fmu/settings/_logging.py +30 -0
- fmu/settings/_version.py +21 -0
- fmu/settings/models/__init__.py +5 -0
- fmu/settings/models/_enums.py +34 -0
- fmu/settings/models/_mappings.py +118 -0
- fmu/settings/models/project_config.py +49 -0
- fmu/settings/models/smda.py +90 -0
- fmu/settings/models/user_config.py +73 -0
- fmu/settings/py.typed +0 -0
- fmu/settings/resources/config_managers.py +211 -0
- fmu/settings/resources/managers.py +96 -0
- fmu/settings/types.py +20 -0
- fmu_settings-0.0.1.dist-info/METADATA +69 -0
- fmu_settings-0.0.1.dist-info/RECORD +21 -0
- fmu_settings-0.0.1.dist-info/WHEEL +5 -0
- fmu_settings-0.0.1.dist-info/licenses/LICENSE +674 -0
- fmu_settings-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""The model for config.json."""
|
|
2
|
+
|
|
3
|
+
import getpass
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
from typing import Self
|
|
6
|
+
from uuid import UUID # noqa TC003
|
|
7
|
+
|
|
8
|
+
from pydantic import AwareDatetime, BaseModel, Field
|
|
9
|
+
|
|
10
|
+
from fmu.settings import __version__
|
|
11
|
+
from fmu.settings.types import ResettableBaseModel, VersionStr # noqa TC001
|
|
12
|
+
|
|
13
|
+
from .smda import Smda
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Masterdata(BaseModel):
|
|
17
|
+
"""The ``masterdata`` block contains information related to masterdata.
|
|
18
|
+
|
|
19
|
+
Currently, SMDA holds the masterdata.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
smda: Smda | None = Field(default=None)
|
|
23
|
+
"""Block containing SMDA-related attributes. See :class:`Smda`."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ProjectConfig(ResettableBaseModel):
|
|
27
|
+
"""The configuration file in a .fmu directory.
|
|
28
|
+
|
|
29
|
+
Stored as config.json.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
version: VersionStr
|
|
33
|
+
created_at: AwareDatetime
|
|
34
|
+
created_by: str
|
|
35
|
+
masterdata: Masterdata
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def reset(cls: type[Self]) -> Self:
|
|
39
|
+
"""Resets the configuration to defaults.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
The new default Config object
|
|
43
|
+
"""
|
|
44
|
+
return cls(
|
|
45
|
+
version=__version__,
|
|
46
|
+
created_at=datetime.now(UTC),
|
|
47
|
+
created_by=getpass.getuser(),
|
|
48
|
+
masterdata=Masterdata(),
|
|
49
|
+
)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Models for SMDA masterdata."""
|
|
2
|
+
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from fmu.settings.types import VersionStr # noqa TC001
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SmdaIdentifier(BaseModel):
|
|
11
|
+
"""The identifier for something known to SMDA."""
|
|
12
|
+
|
|
13
|
+
identifier: str
|
|
14
|
+
"""Identifier known to SMDA."""
|
|
15
|
+
|
|
16
|
+
uuid: UUID
|
|
17
|
+
"""Identifier known to SMDA."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CountryItem(SmdaIdentifier):
|
|
21
|
+
"""A single country in the list of countries known to SMDA."""
|
|
22
|
+
|
|
23
|
+
identifier: str = Field(examples=["Norway"])
|
|
24
|
+
"""Identifier known to SMDA."""
|
|
25
|
+
|
|
26
|
+
uuid: UUID = Field(examples=["15ce3b84-766f-4c93-9050-b154861f9100"])
|
|
27
|
+
"""Identifier known to SMDA."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FieldItem(SmdaIdentifier):
|
|
31
|
+
"""A single field in the list of fields known to SMDA."""
|
|
32
|
+
|
|
33
|
+
identifier: str = Field(examples=["OseFax"])
|
|
34
|
+
"""Identifier known to SMDA."""
|
|
35
|
+
|
|
36
|
+
uuid: UUID = Field(examples=["15ce3b84-766f-4c93-9050-b154861f9100"])
|
|
37
|
+
"""Identifier known to SMDA."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CoordinateSystem(SmdaIdentifier):
|
|
41
|
+
"""Contains the coordinate system known to SMDA."""
|
|
42
|
+
|
|
43
|
+
identifier: str = Field(examples=["ST_WGS84_UTM37N_P32637"])
|
|
44
|
+
"""Identifier known to SMDA."""
|
|
45
|
+
|
|
46
|
+
uuid: UUID = Field(examples=["15ce3b84-766f-4c93-9050-b154861f9100"])
|
|
47
|
+
"""Identifier known to SMDA."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class StratigraphicColumn(SmdaIdentifier):
|
|
51
|
+
"""Contains the stratigraphic column known to SMDA."""
|
|
52
|
+
|
|
53
|
+
identifier: str = Field(examples=["DROGON_2020"])
|
|
54
|
+
"""Identifier known to SMDA."""
|
|
55
|
+
|
|
56
|
+
uuid: UUID = Field(examples=["15ce3b84-766f-4c93-9050-b154861f9100"])
|
|
57
|
+
"""Identifier known to SMDA."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class DiscoveryItem(BaseModel):
|
|
61
|
+
"""A single discovery in the list of discoveries known to SMDA."""
|
|
62
|
+
|
|
63
|
+
short_identifier: str = Field(examples=["SomeDiscovery"])
|
|
64
|
+
"""Identifier known to SMDA."""
|
|
65
|
+
|
|
66
|
+
uuid: UUID = Field(examples=["15ce3b84-766f-4c93-9050-b154861f9100"])
|
|
67
|
+
"""Identifier known to SMDA."""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Smda(BaseModel):
|
|
71
|
+
"""Contains SMDA-related attributes."""
|
|
72
|
+
|
|
73
|
+
coordinate_system: CoordinateSystem
|
|
74
|
+
"""Reference to coordinate system known to SMDA. See :class:`CoordinateSystem`."""
|
|
75
|
+
|
|
76
|
+
country: list[CountryItem]
|
|
77
|
+
"""A list referring to countries known to SMDA. First item is primary.
|
|
78
|
+
See :class:`CountryItem`."""
|
|
79
|
+
|
|
80
|
+
discovery: list[DiscoveryItem]
|
|
81
|
+
"""A list referring to discoveries known to SMDA. First item is primary.
|
|
82
|
+
See :class:`DiscoveryItem`."""
|
|
83
|
+
|
|
84
|
+
field: list[FieldItem]
|
|
85
|
+
"""A list referring to fields known to SMDA. First item is primary.
|
|
86
|
+
See :class:`FieldItem`."""
|
|
87
|
+
|
|
88
|
+
stratigraphic_column: StratigraphicColumn
|
|
89
|
+
"""Reference to stratigraphic column known to SMDA.
|
|
90
|
+
See :class:`StratigraphicColumn`."""
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""The model for config.json."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Annotated, Self
|
|
8
|
+
from uuid import UUID # noqa TC003
|
|
9
|
+
|
|
10
|
+
import annotated_types
|
|
11
|
+
from pydantic import AwareDatetime, BaseModel, SecretStr, field_serializer
|
|
12
|
+
|
|
13
|
+
from fmu.settings import __version__
|
|
14
|
+
from fmu.settings.types import ResettableBaseModel, VersionStr # noqa TC001
|
|
15
|
+
|
|
16
|
+
RecentDirectories = Annotated[set[Path], annotated_types.Len(0, 5)]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class UserAPIKeys(BaseModel):
|
|
20
|
+
"""Known API keys stored in a user config."""
|
|
21
|
+
|
|
22
|
+
smda_subscription: SecretStr | None = None
|
|
23
|
+
|
|
24
|
+
@field_serializer("smda_subscription", when_used="json")
|
|
25
|
+
def dump_secret(self, v: SecretStr | None) -> str | None:
|
|
26
|
+
"""Write the secret string value when serializing to json."""
|
|
27
|
+
if v is None:
|
|
28
|
+
return None
|
|
29
|
+
return v.get_secret_value()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class UserConfig(ResettableBaseModel):
|
|
33
|
+
"""The configuration file in a $HOME/.fmu directory.
|
|
34
|
+
|
|
35
|
+
Stored as config.json.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
version: VersionStr
|
|
39
|
+
created_at: AwareDatetime
|
|
40
|
+
user_api_keys: UserAPIKeys
|
|
41
|
+
recent_directories: RecentDirectories
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def reset(cls: type[Self]) -> Self:
|
|
45
|
+
"""Resets the model to an initial state."""
|
|
46
|
+
return cls(
|
|
47
|
+
version=__version__,
|
|
48
|
+
created_at=datetime.now(UTC),
|
|
49
|
+
user_api_keys=UserAPIKeys(),
|
|
50
|
+
recent_directories=set(),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def obfuscate_secrets(self: Self) -> Self:
|
|
54
|
+
"""Returns a copy of the model with obfuscated secrets.
|
|
55
|
+
|
|
56
|
+
If an API Key is:
|
|
57
|
+
|
|
58
|
+
key: SecretStr = SecretStr("secret")
|
|
59
|
+
|
|
60
|
+
we may want to serialize it to JSON as:
|
|
61
|
+
|
|
62
|
+
{key:"********"}
|
|
63
|
+
|
|
64
|
+
so that we do not serialize the actual value of the secret when, for example,
|
|
65
|
+
returning the user configuration from an API.
|
|
66
|
+
"""
|
|
67
|
+
config_dict = self.model_dump()
|
|
68
|
+
# Overwrite secret keys with obfuscated keys
|
|
69
|
+
for k, v in config_dict["user_api_keys"].items():
|
|
70
|
+
if v is not None:
|
|
71
|
+
# Convert SecretStr("*********") to "*********"
|
|
72
|
+
config_dict["user_api_keys"][k] = str(v)
|
|
73
|
+
return self.model_validate(config_dict)
|
fmu/settings/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,211 @@
|
|
|
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, Any, Final, Self, TypeVar
|
|
7
|
+
from uuid import UUID # noqa TC003
|
|
8
|
+
|
|
9
|
+
from pydantic import ValidationError
|
|
10
|
+
|
|
11
|
+
from fmu.settings._logging import null_logger
|
|
12
|
+
from fmu.settings.models.project_config import ProjectConfig
|
|
13
|
+
from fmu.settings.models.user_config import UserConfig
|
|
14
|
+
from fmu.settings.types import ResettableBaseModel, VersionStr # noqa TC001
|
|
15
|
+
|
|
16
|
+
from .managers import PydanticResourceManager
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
# Avoid circular dependency for type hint in __init__ only
|
|
20
|
+
from fmu.settings._fmu_dir import (
|
|
21
|
+
FMUDirectoryBase,
|
|
22
|
+
ProjectFMUDirectory,
|
|
23
|
+
UserFMUDirectory,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger: Final = null_logger(__name__)
|
|
27
|
+
|
|
28
|
+
T = TypeVar("T", bound=ResettableBaseModel)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ConfigManager(PydanticResourceManager[T]):
|
|
32
|
+
"""Manages the .fmu configuration file."""
|
|
33
|
+
|
|
34
|
+
def __init__(self: Self, fmu_dir: FMUDirectoryBase, config: type[T]) -> None:
|
|
35
|
+
"""Initializes the Config resource manager."""
|
|
36
|
+
super().__init__(fmu_dir, config)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def relative_path(self: Self) -> Path:
|
|
40
|
+
"""Returns the relative path to the config file."""
|
|
41
|
+
return Path("config.json")
|
|
42
|
+
|
|
43
|
+
def _get_dot_notation_key(
|
|
44
|
+
self: Self, config_dict: dict[str, Any], key: str, default: Any = None
|
|
45
|
+
) -> Any:
|
|
46
|
+
"""Sets the value to a dot-notation key.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
config_dict: The configuration dictionary we are modifying (by reference)
|
|
50
|
+
key: The key to set
|
|
51
|
+
default: Value to return if key is not found. Default None
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
The value or default
|
|
55
|
+
"""
|
|
56
|
+
parts = key.split(".")
|
|
57
|
+
value = config_dict
|
|
58
|
+
for part in parts:
|
|
59
|
+
if isinstance(value, dict) and part in value:
|
|
60
|
+
value = value[part]
|
|
61
|
+
else:
|
|
62
|
+
return default
|
|
63
|
+
|
|
64
|
+
return value
|
|
65
|
+
|
|
66
|
+
def get(self: Self, key: str, default: Any = None) -> Any:
|
|
67
|
+
"""Gets a configuration value by key.
|
|
68
|
+
|
|
69
|
+
Supports dot notation for nested values (e.g., "foo.bar")
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
key: The configuration key
|
|
73
|
+
default: Value to return if key is not found. Default None
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
The configuration value or deafult
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
config = self.load()
|
|
80
|
+
|
|
81
|
+
if "." in key:
|
|
82
|
+
return self._get_dot_notation_key(config.model_dump(), key, default)
|
|
83
|
+
|
|
84
|
+
if hasattr(config, key):
|
|
85
|
+
return getattr(config, key)
|
|
86
|
+
|
|
87
|
+
config_dict = config.model_dump()
|
|
88
|
+
return config_dict.get(key, default)
|
|
89
|
+
except FileNotFoundError as e:
|
|
90
|
+
raise FileNotFoundError(
|
|
91
|
+
f"Resource file for '{self.__class__.__name__}' not found "
|
|
92
|
+
f"at: '{self.path}' when getting key {key}"
|
|
93
|
+
) from e
|
|
94
|
+
|
|
95
|
+
def _set_dot_notation_key(
|
|
96
|
+
self: Self, config_dict: dict[str, Any], key: str, value: Any
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Sets the value to a dot-notation key.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
config_dict: The configuration dictionary we are modifying (by reference)
|
|
102
|
+
key: The key to set
|
|
103
|
+
value: The value to set
|
|
104
|
+
"""
|
|
105
|
+
parts = key.split(".")
|
|
106
|
+
target = config_dict
|
|
107
|
+
|
|
108
|
+
for part in parts[:-1]:
|
|
109
|
+
if part not in target or not isinstance(target[part], dict):
|
|
110
|
+
target[part] = {}
|
|
111
|
+
target = target[part]
|
|
112
|
+
|
|
113
|
+
target[parts[-1]] = value
|
|
114
|
+
|
|
115
|
+
def set(self: Self, key: str, value: Any) -> None:
|
|
116
|
+
"""Sets a configuration value by key.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
key: The configuration key
|
|
120
|
+
value: The value to set
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
FileNotFoundError: If config file doesn't exist
|
|
124
|
+
ValueError: If the updated config is invalid
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
config = self.load()
|
|
128
|
+
config_dict = config.model_dump()
|
|
129
|
+
|
|
130
|
+
if "." in key:
|
|
131
|
+
self._set_dot_notation_key(config_dict, key, value)
|
|
132
|
+
else:
|
|
133
|
+
config_dict[key] = value
|
|
134
|
+
|
|
135
|
+
updated_config = config.model_validate(config_dict)
|
|
136
|
+
self.save(updated_config)
|
|
137
|
+
except ValidationError as e:
|
|
138
|
+
raise ValueError(
|
|
139
|
+
f"Invalid value set for '{self.__class__.__name__}' with "
|
|
140
|
+
f"key '{key}', value '{value}': '{e}"
|
|
141
|
+
) from e
|
|
142
|
+
except FileNotFoundError as e:
|
|
143
|
+
raise FileNotFoundError(
|
|
144
|
+
f"Resource file for '{self.__class__.__name__}' not found "
|
|
145
|
+
f"at: '{self.path}' when setting key {key}"
|
|
146
|
+
) from e
|
|
147
|
+
|
|
148
|
+
def update(self: Self, updates: dict[str, Any]) -> T:
|
|
149
|
+
"""Updates multiple configuration values at once.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
updates: Dictionary of key-value pairs to update
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
The updated Config object
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
FileNotFoundError: If config file doesn't exist
|
|
159
|
+
ValueError: If the updates config is invalid
|
|
160
|
+
"""
|
|
161
|
+
try:
|
|
162
|
+
config = self.load()
|
|
163
|
+
config_dict = config.model_dump()
|
|
164
|
+
|
|
165
|
+
flat_updates = {k: v for k, v in updates.items() if "." not in k}
|
|
166
|
+
config_dict.update(flat_updates)
|
|
167
|
+
|
|
168
|
+
for key, value in updates.items():
|
|
169
|
+
if "." in key:
|
|
170
|
+
self._set_dot_notation_key(config_dict, key, value)
|
|
171
|
+
|
|
172
|
+
updated_config = config.model_validate(config_dict)
|
|
173
|
+
self.save(updated_config)
|
|
174
|
+
except ValidationError as e:
|
|
175
|
+
raise ValueError(
|
|
176
|
+
f"Invalid value set for '{self.__class__.__name__}' with "
|
|
177
|
+
f"updates '{updates}': '{e}"
|
|
178
|
+
) from e
|
|
179
|
+
except FileNotFoundError as e:
|
|
180
|
+
raise FileNotFoundError(
|
|
181
|
+
f"Resource file for '{self.__class__.__name__}' not found "
|
|
182
|
+
f"at: '{self.path}' when setting updates {updates}"
|
|
183
|
+
) from e
|
|
184
|
+
|
|
185
|
+
return updated_config
|
|
186
|
+
|
|
187
|
+
def reset(self: Self) -> T:
|
|
188
|
+
"""Resets the configuration to defaults.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
The new default config object
|
|
192
|
+
"""
|
|
193
|
+
config = self.model_class.reset()
|
|
194
|
+
self.save(config)
|
|
195
|
+
return config
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class ProjectConfigManager(ConfigManager[ProjectConfig]):
|
|
199
|
+
"""Manages the .fmu configuration file in a project."""
|
|
200
|
+
|
|
201
|
+
def __init__(self: Self, fmu_dir: ProjectFMUDirectory) -> None:
|
|
202
|
+
"""Initializes the ProjectConfig resource manager."""
|
|
203
|
+
super().__init__(fmu_dir, ProjectConfig)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class UserConfigManager(ConfigManager[UserConfig]):
|
|
207
|
+
"""Manages the .fmu configuration file in a user's home directory."""
|
|
208
|
+
|
|
209
|
+
def __init__(self: Self, fmu_dir: UserFMUDirectory) -> None:
|
|
210
|
+
"""Initializes the UserConfig resource manager."""
|
|
211
|
+
super().__init__(fmu_dir, UserConfig)
|
|
@@ -0,0 +1,96 @@
|
|
|
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) -> T:
|
|
52
|
+
"""Loads the resources 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
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Validated Pydantic model
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
FileNotFoundError: If the resource file doesn't exist
|
|
62
|
+
ValidationError: If the data doesn't match the model schema
|
|
63
|
+
"""
|
|
64
|
+
if self._cache is None or force:
|
|
65
|
+
if not self.exists:
|
|
66
|
+
raise FileNotFoundError(
|
|
67
|
+
f"Resource file for '{self.__class__.__name__}' not found "
|
|
68
|
+
f"at: '{self.path}'"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
content = self.fmu_dir.read_text_file(self.relative_path)
|
|
73
|
+
data = json.loads(content)
|
|
74
|
+
self._cache = self.model_class.model_validate(data)
|
|
75
|
+
except ValidationError as e:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"Invalid content in resource file for '{self.__class__.__name__}: "
|
|
78
|
+
f"'{e}"
|
|
79
|
+
) from e
|
|
80
|
+
except json.JSONDecodeError as e:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"Invalid JSON in resource file for '{self.__class__.__name__}': "
|
|
83
|
+
f"'{e}'"
|
|
84
|
+
) from e
|
|
85
|
+
|
|
86
|
+
return self._cache
|
|
87
|
+
|
|
88
|
+
def save(self: Self, model: T) -> None:
|
|
89
|
+
"""Save the Pydantic model to disk.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
model: Validated Pydantic model instance
|
|
93
|
+
"""
|
|
94
|
+
json_data = model.model_dump_json(by_alias=True, indent=2)
|
|
95
|
+
self.fmu_dir.write_text_file(self.relative_path, json_data)
|
|
96
|
+
self._cache = model
|
fmu/settings/types.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Type annotations used in Pydantic models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated, Self, TypeAlias
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
VersionStr: TypeAlias = Annotated[
|
|
10
|
+
str, Field(pattern=r"(\d+(\.\d+){0,2}|\d+\.\d+\.[a-z0-9]+\+[a-z0-9.]+)")
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ResettableBaseModel(BaseModel):
|
|
15
|
+
"""A Pydantic BaseModel that implements reset()."""
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def reset(cls) -> Self:
|
|
19
|
+
"""Resets the model to an initial state."""
|
|
20
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fmu-settings
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A library for managing FMU settings
|
|
5
|
+
Author-email: Equinor <fg-fmu_atlas@equinor.com>
|
|
6
|
+
License: GPL-3.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/equinor/fmu-settings
|
|
8
|
+
Project-URL: Repository, https://github.com/equinor/fmu-settings
|
|
9
|
+
Project-URL: Documentation, https://github.com/equinor/fmu-settings
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering
|
|
12
|
+
Classifier: Topic :: Utilities
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Natural Language :: English
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: annotated_types
|
|
21
|
+
Requires-Dist: fmu-dataio
|
|
22
|
+
Requires-Dist: pydantic
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: mypy; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-mock; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-xdist; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# fmu-settings
|
|
33
|
+
|
|
34
|
+
[](https://github.com/equinor/fmu-settings/actions/workflows/ci.yml)
|
|
35
|
+
|
|
36
|
+
**fmu-settings** is a package to manage and interface with `.fmu/`
|
|
37
|
+
directories, where the FMU settings are contained.
|
|
38
|
+
|
|
39
|
+
## Developing
|
|
40
|
+
|
|
41
|
+
Clone and install into a virtual environment.
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
git clone git@github.com:equinor/fmu-settings.git
|
|
45
|
+
cd fmu-settings
|
|
46
|
+
# Create or source virtual/Komodo env
|
|
47
|
+
pip install -U pip
|
|
48
|
+
pip install -e ".[dev]"
|
|
49
|
+
# Make a feature branch for your changes
|
|
50
|
+
git checkout -b some-feature-branch
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Run the tests with
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
pytest -n auto tests
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Ensure your changes will pass the various linters before making a pull
|
|
60
|
+
request. It is expected that all code will be typed and validated with
|
|
61
|
+
mypy.
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
ruff check
|
|
65
|
+
ruff format --check
|
|
66
|
+
mypy src tests
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
See the [contributing document](CONTRIBUTING.md) for more.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
fmu/__init__.py,sha256=htx6HlMme77I6pZ8U256-2B2cMJuELsu3JN3YM2Efh4,144
|
|
2
|
+
fmu/settings/__init__.py,sha256=x96dVVR-2n2lYD84LGbL7W8l3-r7W_0reUTKZlE7S34,331
|
|
3
|
+
fmu/settings/_fmu_dir.py,sha256=-w3cB0_2WCKYkXTmoOQtZHI_fHfCDbnzEtTF_lcYod8,10572
|
|
4
|
+
fmu/settings/_init.py,sha256=5CT7tV2XHz5wuLh97XozyLiKpwogrsfjpxm2dpn7KWE,4097
|
|
5
|
+
fmu/settings/_logging.py,sha256=nEdmZlNCBsB1GfDmFMKCjZmeuRp3CRlbz1EYUemc95Y,1104
|
|
6
|
+
fmu/settings/_version.py,sha256=vgltXBYF55vNcC2regxjGN0_cbebmm8VgcDdQaDapWQ,511
|
|
7
|
+
fmu/settings/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
fmu/settings/types.py,sha256=aeXEsznBTT1YRRY_LSRqK1j2gmMmyLYYTGYl3a9fweU,513
|
|
9
|
+
fmu/settings/models/__init__.py,sha256=lRlXgl55ba2upmDzdvzx8N30JMq2Osnm8aa_xxTZn8A,112
|
|
10
|
+
fmu/settings/models/_enums.py,sha256=SQUZ-2mQcTx4F0oefPFfuQzMKsKTSFSB-wq_CH7TBRE,734
|
|
11
|
+
fmu/settings/models/_mappings.py,sha256=uaSAE_0y88Gvv-vM3VR5xx7yuXn9mio_8V3YC1t9wLI,2836
|
|
12
|
+
fmu/settings/models/project_config.py,sha256=vImpk_rPrMEn3G9NZj83Recq9YzqAABQS7ZVUlBa0vM,1207
|
|
13
|
+
fmu/settings/models/smda.py,sha256=nQ3-EI2VDRappwHgipLLXnWbEVQCsLrtJn14lES2Q6g,2639
|
|
14
|
+
fmu/settings/models/user_config.py,sha256=JhMeSmWcE4GrBRkM_D5QVnUbRKfVy_XakHeKqJrYxvE,2217
|
|
15
|
+
fmu/settings/resources/config_managers.py,sha256=4rA2xXS8BoNkM-RCfhJ6FNiwRGW_jTckZzpT_llkqlY,6852
|
|
16
|
+
fmu/settings/resources/managers.py,sha256=t4Rp6MOSIq85iKfDHZLJxeGRj8S3SKanWHWri0p9wV8,3161
|
|
17
|
+
fmu_settings-0.0.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
18
|
+
fmu_settings-0.0.1.dist-info/METADATA,sha256=_Jt3s4v_3G-lhNAd4S8ocOYZ0_KoRQEdCRCIvaQqO68,2020
|
|
19
|
+
fmu_settings-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
20
|
+
fmu_settings-0.0.1.dist-info/top_level.txt,sha256=Z-FIY3pxn0UK2Wxi9IJ7fKoLSraaxuNGi1eokiE0ShM,4
|
|
21
|
+
fmu_settings-0.0.1.dist-info/RECORD,,
|