fmu-settings 0.5.2__py3-none-any.whl → 0.14.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.
- fmu/settings/_fmu_dir.py +251 -19
- fmu/settings/_init.py +19 -32
- fmu/settings/_readme_texts.py +34 -0
- fmu/settings/_resources/cache_manager.py +185 -0
- fmu/settings/_resources/changelog_manager.py +157 -0
- fmu/settings/_resources/config_managers.py +17 -0
- fmu/settings/_resources/lock_manager.py +33 -17
- fmu/settings/_resources/log_manager.py +98 -0
- fmu/settings/_resources/pydantic_resource_manager.py +173 -27
- fmu/settings/_resources/user_session_log_manager.py +47 -0
- fmu/settings/_version.py +2 -2
- fmu/settings/models/_enums.py +18 -0
- fmu/settings/models/change_info.py +37 -0
- fmu/settings/models/event_info.py +15 -0
- fmu/settings/models/log.py +63 -0
- fmu/settings/models/project_config.py +58 -4
- fmu/settings/models/user_config.py +5 -0
- {fmu_settings-0.5.2.dist-info → fmu_settings-0.14.1.dist-info}/METADATA +3 -1
- fmu_settings-0.14.1.dist-info/RECORD +32 -0
- fmu_settings-0.5.2.dist-info/RECORD +0 -24
- {fmu_settings-0.5.2.dist-info → fmu_settings-0.14.1.dist-info}/WHEEL +0 -0
- {fmu_settings-0.5.2.dist-info → fmu_settings-0.14.1.dist-info}/licenses/LICENSE +0 -0
- {fmu_settings-0.5.2.dist-info → fmu_settings-0.14.1.dist-info}/top_level.txt +0 -0
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import copy
|
|
5
6
|
import json
|
|
6
|
-
from
|
|
7
|
+
from builtins import TypeError
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Final, Generic, Self, TypeVar
|
|
7
9
|
|
|
8
10
|
from pydantic import BaseModel, ValidationError
|
|
9
11
|
|
|
12
|
+
from fmu.settings.models.project_config import ProjectConfig
|
|
10
13
|
from fmu.settings.types import ResettableBaseModel
|
|
11
14
|
|
|
12
15
|
if TYPE_CHECKING:
|
|
@@ -22,6 +25,8 @@ MutablePydanticResource = TypeVar("MutablePydanticResource", bound=ResettableBas
|
|
|
22
25
|
class PydanticResourceManager(Generic[PydanticResource]):
|
|
23
26
|
"""Base class for managing resources represented by Pydantic models."""
|
|
24
27
|
|
|
28
|
+
automatic_caching: bool = True
|
|
29
|
+
|
|
25
30
|
def __init__(
|
|
26
31
|
self: Self, fmu_dir: FMUDirectoryBase, model_class: type[PydanticResource]
|
|
27
32
|
) -> None:
|
|
@@ -53,6 +58,30 @@ class PydanticResourceManager(Generic[PydanticResource]):
|
|
|
53
58
|
"""Returns whether or not the resource exists."""
|
|
54
59
|
return self.path.exists()
|
|
55
60
|
|
|
61
|
+
@staticmethod
|
|
62
|
+
def _get_dot_notation_key(
|
|
63
|
+
resource_dict: dict[str, Any], key: str, default: Any = None
|
|
64
|
+
) -> Any:
|
|
65
|
+
"""Get a value from the resource by a dot-notation key.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
resource_dict: The resource dictionary to get the value from
|
|
69
|
+
key: The key to the value in the resource
|
|
70
|
+
default: Value to return if key is not found. Default None
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
The value or default
|
|
74
|
+
"""
|
|
75
|
+
parts = key.split(".")
|
|
76
|
+
value = resource_dict
|
|
77
|
+
for part in parts:
|
|
78
|
+
if isinstance(value, dict) and part in value:
|
|
79
|
+
value = value[part]
|
|
80
|
+
else:
|
|
81
|
+
return default
|
|
82
|
+
|
|
83
|
+
return value
|
|
84
|
+
|
|
56
85
|
def load(
|
|
57
86
|
self: Self, force: bool = False, store_cache: bool = True
|
|
58
87
|
) -> PydanticResource:
|
|
@@ -99,17 +128,103 @@ class PydanticResourceManager(Generic[PydanticResource]):
|
|
|
99
128
|
|
|
100
129
|
return self._cache
|
|
101
130
|
|
|
102
|
-
def save(
|
|
131
|
+
def save(
|
|
132
|
+
self: Self,
|
|
133
|
+
model: PydanticResource,
|
|
134
|
+
) -> None:
|
|
103
135
|
"""Save the Pydantic model to disk.
|
|
104
136
|
|
|
105
137
|
Args:
|
|
106
|
-
model: Validated Pydantic model instance
|
|
138
|
+
model: Validated Pydantic model instance.
|
|
107
139
|
"""
|
|
108
140
|
self.fmu_dir._lock.ensure_can_write()
|
|
141
|
+
|
|
109
142
|
json_data = model.model_dump_json(by_alias=True, indent=2)
|
|
110
143
|
self.fmu_dir.write_text_file(self.relative_path, json_data)
|
|
144
|
+
|
|
145
|
+
if self.automatic_caching and self.exists:
|
|
146
|
+
self.fmu_dir.cache.store_revision(self.relative_path, json_data)
|
|
147
|
+
|
|
111
148
|
self._cache = model
|
|
112
149
|
|
|
150
|
+
@staticmethod
|
|
151
|
+
def get_model_diff(
|
|
152
|
+
current_model: BaseModel, incoming_model: BaseModel, prefix: str = ""
|
|
153
|
+
) -> list[tuple[str, Any, Any]]:
|
|
154
|
+
"""Recursively get differences between two Pydantic models.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
A list of differences between the models on field level.
|
|
158
|
+
"""
|
|
159
|
+
if type(incoming_model) is not type(current_model):
|
|
160
|
+
raise ValueError(
|
|
161
|
+
"Models must be of the same type. Current model is of type "
|
|
162
|
+
f"'{current_model.__class__.__name__}', incoming model of type "
|
|
163
|
+
f"'{incoming_model.__class__.__name__}'."
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
IGNORED_FIELDS: Final[list[str]] = [
|
|
167
|
+
"created_at",
|
|
168
|
+
"created_by",
|
|
169
|
+
"last_modified_at",
|
|
170
|
+
"last_modified_by",
|
|
171
|
+
]
|
|
172
|
+
changes: list[tuple[str, Any, Any]] = []
|
|
173
|
+
|
|
174
|
+
for field_name in type(current_model).model_fields:
|
|
175
|
+
if field_name in IGNORED_FIELDS:
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
current_value = getattr(current_model, field_name)
|
|
179
|
+
incoming_value = getattr(incoming_model, field_name)
|
|
180
|
+
|
|
181
|
+
field_path = f"{prefix}.{field_name}" if prefix else field_name
|
|
182
|
+
|
|
183
|
+
if current_value is None and incoming_value is not None:
|
|
184
|
+
changes.append((field_path, None, incoming_value))
|
|
185
|
+
elif current_value is not None and incoming_value is None:
|
|
186
|
+
changes.append((field_path, current_value, None))
|
|
187
|
+
elif isinstance(current_value, BaseModel) and isinstance(
|
|
188
|
+
incoming_value, BaseModel
|
|
189
|
+
):
|
|
190
|
+
changes.extend(
|
|
191
|
+
PydanticResourceManager.get_model_diff(
|
|
192
|
+
current_value, incoming_value, field_path
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
elif isinstance(current_value, list) and isinstance(incoming_value, list):
|
|
196
|
+
if current_value != incoming_value:
|
|
197
|
+
changes.append((field_path, current_value, incoming_value))
|
|
198
|
+
elif current_value != incoming_value:
|
|
199
|
+
changes.append((field_path, current_value, incoming_value))
|
|
200
|
+
|
|
201
|
+
return changes
|
|
202
|
+
|
|
203
|
+
def get_resource_diff(
|
|
204
|
+
self: Self, incoming_resource: PydanticResourceManager[PydanticResource]
|
|
205
|
+
) -> list[tuple[str, Any, Any]]:
|
|
206
|
+
"""Get differences between current and incoming Pydantic resource.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
A list of differences between the resources.
|
|
210
|
+
"""
|
|
211
|
+
if self.exists and incoming_resource.exists:
|
|
212
|
+
current_model = self.load()
|
|
213
|
+
incoming_model = incoming_resource.load()
|
|
214
|
+
if type(current_model) is not type(incoming_model):
|
|
215
|
+
raise TypeError(
|
|
216
|
+
f"Resources to diff must be of the same type. Current resource is "
|
|
217
|
+
f"of type '{self.model_class.__name__}', incoming resource of type "
|
|
218
|
+
f"'{incoming_model.__class__.__name__}'."
|
|
219
|
+
)
|
|
220
|
+
return self.get_model_diff(current_model, incoming_model)
|
|
221
|
+
raise FileNotFoundError(
|
|
222
|
+
"Resources to diff must exist in both directories: "
|
|
223
|
+
f"Current resource {str(self.relative_path)} exists: {self.exists}. "
|
|
224
|
+
f"Incoming resource {str(incoming_resource.relative_path)} exists: "
|
|
225
|
+
f"{incoming_resource.exists}."
|
|
226
|
+
)
|
|
227
|
+
|
|
113
228
|
|
|
114
229
|
class MutablePydanticResourceManager(PydanticResourceManager[MutablePydanticResource]):
|
|
115
230
|
"""Manages the .fmu resource file."""
|
|
@@ -120,29 +235,6 @@ class MutablePydanticResourceManager(PydanticResourceManager[MutablePydanticReso
|
|
|
120
235
|
"""Initializes the resource manager."""
|
|
121
236
|
super().__init__(fmu_dir, resource)
|
|
122
237
|
|
|
123
|
-
def _get_dot_notation_key(
|
|
124
|
-
self: Self, resource_dict: dict[str, Any], key: str, default: Any = None
|
|
125
|
-
) -> Any:
|
|
126
|
-
"""Sets the value to a dot-notation key.
|
|
127
|
-
|
|
128
|
-
Args:
|
|
129
|
-
resource_dict: The resource 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 = resource_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
238
|
def get(self: Self, key: str, default: Any = None) -> Any:
|
|
147
239
|
"""Gets a resource value by key.
|
|
148
240
|
|
|
@@ -206,6 +298,7 @@ class MutablePydanticResourceManager(PydanticResourceManager[MutablePydanticReso
|
|
|
206
298
|
try:
|
|
207
299
|
resource = self.load()
|
|
208
300
|
resource_dict = resource.model_dump()
|
|
301
|
+
old_resource_dict = copy.deepcopy(resource_dict)
|
|
209
302
|
|
|
210
303
|
if "." in key:
|
|
211
304
|
self._set_dot_notation_key(resource_dict, key, value)
|
|
@@ -214,6 +307,14 @@ class MutablePydanticResourceManager(PydanticResourceManager[MutablePydanticReso
|
|
|
214
307
|
|
|
215
308
|
updated_resource = resource.model_validate(resource_dict)
|
|
216
309
|
self.save(updated_resource)
|
|
310
|
+
|
|
311
|
+
if self.model_class == ProjectConfig:
|
|
312
|
+
self.fmu_dir._changelog.log_update_to_changelog(
|
|
313
|
+
updates={key: value},
|
|
314
|
+
old_resource_dict=old_resource_dict,
|
|
315
|
+
relative_path=self.relative_path,
|
|
316
|
+
)
|
|
317
|
+
|
|
217
318
|
except ValidationError as e:
|
|
218
319
|
raise ValueError(
|
|
219
320
|
f"Invalid value set for '{self.__class__.__name__}' with "
|
|
@@ -241,6 +342,7 @@ class MutablePydanticResourceManager(PydanticResourceManager[MutablePydanticReso
|
|
|
241
342
|
try:
|
|
242
343
|
resource = self.load()
|
|
243
344
|
resource_dict = resource.model_dump()
|
|
345
|
+
old_resource_dict = copy.deepcopy(resource_dict)
|
|
244
346
|
|
|
245
347
|
flat_updates = {k: v for k, v in updates.items() if "." not in k}
|
|
246
348
|
resource_dict.update(flat_updates)
|
|
@@ -251,6 +353,12 @@ class MutablePydanticResourceManager(PydanticResourceManager[MutablePydanticReso
|
|
|
251
353
|
|
|
252
354
|
updated_resource = resource.model_validate(resource_dict)
|
|
253
355
|
self.save(updated_resource)
|
|
356
|
+
|
|
357
|
+
if self.model_class == ProjectConfig:
|
|
358
|
+
self.fmu_dir._changelog.log_update_to_changelog(
|
|
359
|
+
updates, old_resource_dict, self.relative_path
|
|
360
|
+
)
|
|
361
|
+
|
|
254
362
|
except ValidationError as e:
|
|
255
363
|
raise ValueError(
|
|
256
364
|
f"Invalid value set for '{self.__class__.__name__}' with "
|
|
@@ -262,7 +370,7 @@ class MutablePydanticResourceManager(PydanticResourceManager[MutablePydanticReso
|
|
|
262
370
|
f"at: '{self.path}' when setting updates {updates}"
|
|
263
371
|
) from e
|
|
264
372
|
|
|
265
|
-
return
|
|
373
|
+
return self.load()
|
|
266
374
|
|
|
267
375
|
def reset(self: Self) -> MutablePydanticResource:
|
|
268
376
|
"""Resets the resources to defaults.
|
|
@@ -273,3 +381,41 @@ class MutablePydanticResourceManager(PydanticResourceManager[MutablePydanticReso
|
|
|
273
381
|
resource = self.model_class.reset()
|
|
274
382
|
self.save(resource)
|
|
275
383
|
return resource
|
|
384
|
+
|
|
385
|
+
def merge_resource(
|
|
386
|
+
self: Self,
|
|
387
|
+
incoming_resource: MutablePydanticResourceManager[MutablePydanticResource],
|
|
388
|
+
) -> MutablePydanticResource:
|
|
389
|
+
"""Merge an incoming Pydantic resource into the current resource model.
|
|
390
|
+
|
|
391
|
+
All changes in the incoming resource will be applied.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
The updated resource object
|
|
395
|
+
"""
|
|
396
|
+
try:
|
|
397
|
+
changes: list[tuple[str, Any, Any]] = self.get_resource_diff(
|
|
398
|
+
incoming_resource
|
|
399
|
+
)
|
|
400
|
+
return self.merge_changes(changes)
|
|
401
|
+
except TypeError as e:
|
|
402
|
+
raise TypeError(
|
|
403
|
+
f"Merging pydantic resource failed. The incoming resource must be of "
|
|
404
|
+
f"type '{self.model_class.__name__}'. The provided model was of type "
|
|
405
|
+
f"'{incoming_resource.model_class.__name__}'."
|
|
406
|
+
) from e
|
|
407
|
+
|
|
408
|
+
def merge_changes(
|
|
409
|
+
self: Self, changes: list[tuple[str, Any, Any]]
|
|
410
|
+
) -> MutablePydanticResource:
|
|
411
|
+
"""Merge a list of changes into the current resource model.
|
|
412
|
+
|
|
413
|
+
All changes will overwrite the current values.
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
The updated resource object
|
|
417
|
+
"""
|
|
418
|
+
updates: dict[str, Any] = {}
|
|
419
|
+
for change in changes:
|
|
420
|
+
updates[change[0]] = change[2]
|
|
421
|
+
return self.update(updates)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Manages an .fmu user_session_log file."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING, Self
|
|
8
|
+
|
|
9
|
+
from fmu.settings._resources.log_manager import LogManager
|
|
10
|
+
from fmu.settings.models.event_info import EventInfo
|
|
11
|
+
from fmu.settings.models.log import Log, LogFileName
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
# Avoid circular dependency for type hint in __init__ only
|
|
15
|
+
from fmu.settings._fmu_dir import (
|
|
16
|
+
FMUDirectoryBase,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class UserSessionLogManager(LogManager[EventInfo]):
|
|
21
|
+
"""Manages the .fmu user session log file."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self: Self, fmu_dir: FMUDirectoryBase, retention_days: int | None = None
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Initializes the User session log resource manager."""
|
|
27
|
+
super().__init__(fmu_dir, Log[EventInfo])
|
|
28
|
+
|
|
29
|
+
# If user_session_log.json exists from previous session, cache it and delete
|
|
30
|
+
# We want a fresh log each time
|
|
31
|
+
if self.exists:
|
|
32
|
+
self.fmu_dir._lock.ensure_can_write()
|
|
33
|
+
content = self.fmu_dir.read_text_file(self.relative_path)
|
|
34
|
+
self.fmu_dir.cache.store_revision(
|
|
35
|
+
self.relative_path, content, skip_trim=True
|
|
36
|
+
)
|
|
37
|
+
with contextlib.suppress(FileNotFoundError):
|
|
38
|
+
self.path.unlink()
|
|
39
|
+
|
|
40
|
+
self.fmu_dir.cache.trim_by_age(
|
|
41
|
+
self.relative_path, retention_days or self.fmu_dir.cache.RETENTION_DAYS
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def relative_path(self: Self) -> Path:
|
|
46
|
+
"""Returns the relative path to the log file."""
|
|
47
|
+
return Path("logs") / LogFileName.user_session_log
|
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.14.1'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 14, 1)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
fmu/settings/models/_enums.py
CHANGED
|
@@ -32,3 +32,21 @@ class DataEntrySource(StrEnum):
|
|
|
32
32
|
|
|
33
33
|
class TargetSystem(StrEnum):
|
|
34
34
|
smda = "smda"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ChangeType(StrEnum):
|
|
38
|
+
"""The types of change that can be made on a file."""
|
|
39
|
+
|
|
40
|
+
update = "update"
|
|
41
|
+
remove = "remove"
|
|
42
|
+
add = "add"
|
|
43
|
+
reset = "reset"
|
|
44
|
+
merge = "merge"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class FilterType(StrEnum):
|
|
48
|
+
"""The supported types to filter on."""
|
|
49
|
+
|
|
50
|
+
date = "date"
|
|
51
|
+
number = "number"
|
|
52
|
+
text = "text"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Model for the log entries in the the changelog file."""
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from pydantic import AwareDatetime, BaseModel, Field, field_validator
|
|
8
|
+
|
|
9
|
+
from fmu.settings.models._enums import ChangeType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ChangeInfo(BaseModel):
|
|
13
|
+
"""Represents a change in the changelog file."""
|
|
14
|
+
|
|
15
|
+
timestamp: AwareDatetime = Field(
|
|
16
|
+
default_factory=lambda: datetime.now(UTC), strict=True
|
|
17
|
+
)
|
|
18
|
+
change_type: ChangeType
|
|
19
|
+
user: str
|
|
20
|
+
path: Path
|
|
21
|
+
change: str
|
|
22
|
+
hostname: str
|
|
23
|
+
file: str
|
|
24
|
+
key: str
|
|
25
|
+
|
|
26
|
+
@field_validator("timestamp", mode="before")
|
|
27
|
+
@classmethod
|
|
28
|
+
def convert_timestamp(cls, value: str) -> AwareDatetime:
|
|
29
|
+
"""Convert timestamp values given as a 'str' or Pandas 'Timestamp'.
|
|
30
|
+
|
|
31
|
+
Values of other types will be handled by Pydantic.
|
|
32
|
+
"""
|
|
33
|
+
if isinstance(value, str):
|
|
34
|
+
return datetime.fromisoformat(value)
|
|
35
|
+
if isinstance(value, pd.Timestamp):
|
|
36
|
+
return value.to_pydatetime()
|
|
37
|
+
return value
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Model for the log entries in the the userlog file."""
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
|
|
5
|
+
from pydantic import AwareDatetime, BaseModel, ConfigDict, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EventInfo(BaseModel):
|
|
9
|
+
"""Represents information about user session activity."""
|
|
10
|
+
|
|
11
|
+
model_config = ConfigDict(extra="allow")
|
|
12
|
+
|
|
13
|
+
level: str = "INFO"
|
|
14
|
+
event: str = "unknown"
|
|
15
|
+
timestamp: AwareDatetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Module for the log file and related models."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from typing import Any, Literal, Self, TypeVar
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field, RootModel
|
|
8
|
+
|
|
9
|
+
from fmu.settings.models._enums import FilterType
|
|
10
|
+
|
|
11
|
+
LogEntryType = TypeVar("LogEntryType", bound=BaseModel)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Log(RootModel[list[LogEntryType]]):
|
|
15
|
+
"""Represents a log file in a .fmu directory."""
|
|
16
|
+
|
|
17
|
+
root: list[LogEntryType] = Field(default_factory=list)
|
|
18
|
+
|
|
19
|
+
def add_entry(self: Self, entry: LogEntryType) -> None:
|
|
20
|
+
"""Adds a log entry to the log."""
|
|
21
|
+
self.root.append(entry)
|
|
22
|
+
|
|
23
|
+
def __getitem__(self: Self, index: int) -> LogEntryType:
|
|
24
|
+
"""Retrieves a log entry from the log using the specified index."""
|
|
25
|
+
return self.root[index]
|
|
26
|
+
|
|
27
|
+
def __iter__(self: Self) -> Any:
|
|
28
|
+
"""Returns an iterator for the log."""
|
|
29
|
+
return iter(self.root)
|
|
30
|
+
|
|
31
|
+
def __len__(self: Self) -> int:
|
|
32
|
+
"""Returns the number of log entries in the log."""
|
|
33
|
+
return len(self.root)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Filter(BaseModel):
|
|
37
|
+
"""Represents a filter that can be applied on a log file."""
|
|
38
|
+
|
|
39
|
+
field_name: str
|
|
40
|
+
filter_value: str
|
|
41
|
+
filter_type: FilterType
|
|
42
|
+
operator: Literal[">", ">=", "<", "<=", "==", "!="]
|
|
43
|
+
|
|
44
|
+
def parse_filter_value(self: Self) -> str | datetime | int:
|
|
45
|
+
"""Parse filter value to its type."""
|
|
46
|
+
match self.filter_type:
|
|
47
|
+
case FilterType.date:
|
|
48
|
+
return datetime.fromisoformat(self.filter_value)
|
|
49
|
+
case FilterType.number:
|
|
50
|
+
return int(self.filter_value)
|
|
51
|
+
case FilterType.text:
|
|
52
|
+
return self.filter_value
|
|
53
|
+
case _:
|
|
54
|
+
raise ValueError(
|
|
55
|
+
"Invalid filter type supplied when parsing filter value."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class LogFileName(StrEnum):
|
|
60
|
+
"""The log files in the .fmu directory."""
|
|
61
|
+
|
|
62
|
+
changelog = "changelog.json"
|
|
63
|
+
user_session_log = "user_session_log.json"
|
|
@@ -2,15 +2,61 @@
|
|
|
2
2
|
|
|
3
3
|
import getpass
|
|
4
4
|
from datetime import UTC, datetime
|
|
5
|
+
from pathlib import Path
|
|
5
6
|
from typing import Self
|
|
6
7
|
|
|
7
|
-
from pydantic import AwareDatetime, Field
|
|
8
|
+
from pydantic import AwareDatetime, BaseModel, Field
|
|
8
9
|
|
|
9
10
|
from fmu.datamodels.fmu_results.fields import Access, Masterdata, Model
|
|
10
11
|
from fmu.settings import __version__
|
|
11
12
|
from fmu.settings.types import ResettableBaseModel, VersionStr # noqa: TC001
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
class RmsCoordinateSystem(BaseModel):
|
|
16
|
+
"""The project coordinate system of an RMS project."""
|
|
17
|
+
|
|
18
|
+
name: str
|
|
19
|
+
"""Name of the coordinate system."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RmsStratigraphicZone(BaseModel):
|
|
23
|
+
"""A stratigraphic zone from an RMS project."""
|
|
24
|
+
|
|
25
|
+
name: str
|
|
26
|
+
"""Name of the zone."""
|
|
27
|
+
|
|
28
|
+
top_horizon_name: str
|
|
29
|
+
"""Name of the horizon at the top of the zone."""
|
|
30
|
+
|
|
31
|
+
base_horizon_name: str
|
|
32
|
+
"""Name of the horizon at the base of the zone."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RmsHorizon(BaseModel):
|
|
36
|
+
"""A horizon from an RMS project."""
|
|
37
|
+
|
|
38
|
+
name: str
|
|
39
|
+
"""Name of the horizon."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class RmsWell(BaseModel):
|
|
43
|
+
"""A well from an RMS project."""
|
|
44
|
+
|
|
45
|
+
name: str
|
|
46
|
+
"""Name of the well."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class RmsProject(BaseModel):
|
|
50
|
+
"""RMS project configuration."""
|
|
51
|
+
|
|
52
|
+
path: Path
|
|
53
|
+
version: str
|
|
54
|
+
coordinate_system: RmsCoordinateSystem | None = None
|
|
55
|
+
zones: list[RmsStratigraphicZone] | None = None
|
|
56
|
+
horizons: list[RmsHorizon] | None = None
|
|
57
|
+
wells: list[RmsWell] | None = None
|
|
58
|
+
|
|
59
|
+
|
|
14
60
|
class ProjectConfig(ResettableBaseModel):
|
|
15
61
|
"""The configuration file in a .fmu directory.
|
|
16
62
|
|
|
@@ -20,9 +66,13 @@ class ProjectConfig(ResettableBaseModel):
|
|
|
20
66
|
version: VersionStr
|
|
21
67
|
created_at: AwareDatetime
|
|
22
68
|
created_by: str
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
69
|
+
last_modified_at: AwareDatetime | None = None
|
|
70
|
+
last_modified_by: str | None = None
|
|
71
|
+
masterdata: Masterdata | None = None
|
|
72
|
+
model: Model | None = None
|
|
73
|
+
access: Access | None = None
|
|
74
|
+
cache_max_revisions: int = Field(default=5, ge=5)
|
|
75
|
+
rms: RmsProject | None = None
|
|
26
76
|
|
|
27
77
|
@classmethod
|
|
28
78
|
def reset(cls: type[Self]) -> Self:
|
|
@@ -35,7 +85,11 @@ class ProjectConfig(ResettableBaseModel):
|
|
|
35
85
|
version=__version__,
|
|
36
86
|
created_at=datetime.now(UTC),
|
|
37
87
|
created_by=getpass.getuser(),
|
|
88
|
+
last_modified_at=None,
|
|
89
|
+
last_modified_by=None,
|
|
38
90
|
masterdata=None,
|
|
39
91
|
model=None,
|
|
40
92
|
access=None,
|
|
93
|
+
cache_max_revisions=5,
|
|
94
|
+
rms=None,
|
|
41
95
|
)
|
|
@@ -10,6 +10,7 @@ import annotated_types
|
|
|
10
10
|
from pydantic import (
|
|
11
11
|
AwareDatetime,
|
|
12
12
|
BaseModel,
|
|
13
|
+
Field,
|
|
13
14
|
SecretStr,
|
|
14
15
|
field_serializer,
|
|
15
16
|
field_validator,
|
|
@@ -42,6 +43,8 @@ class UserConfig(ResettableBaseModel):
|
|
|
42
43
|
|
|
43
44
|
version: VersionStr
|
|
44
45
|
created_at: AwareDatetime
|
|
46
|
+
last_modified_at: AwareDatetime | None = None
|
|
47
|
+
cache_max_revisions: int = Field(default=5, ge=5)
|
|
45
48
|
user_api_keys: UserAPIKeys
|
|
46
49
|
recent_project_directories: RecentProjectDirectories
|
|
47
50
|
|
|
@@ -51,6 +54,8 @@ class UserConfig(ResettableBaseModel):
|
|
|
51
54
|
return cls(
|
|
52
55
|
version=__version__,
|
|
53
56
|
created_at=datetime.now(UTC),
|
|
57
|
+
last_modified_at=None,
|
|
58
|
+
cache_max_revisions=5,
|
|
54
59
|
user_api_keys=UserAPIKeys(),
|
|
55
60
|
recent_project_directories=[],
|
|
56
61
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fmu-settings
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.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
|
|
@@ -21,9 +21,11 @@ Requires-Dist: PyYAML
|
|
|
21
21
|
Requires-Dist: annotated_types
|
|
22
22
|
Requires-Dist: fmu-config
|
|
23
23
|
Requires-Dist: fmu-datamodels
|
|
24
|
+
Requires-Dist: pandas
|
|
24
25
|
Requires-Dist: pydantic
|
|
25
26
|
Provides-Extra: dev
|
|
26
27
|
Requires-Dist: mypy; extra == "dev"
|
|
28
|
+
Requires-Dist: pandas-stubs; extra == "dev"
|
|
27
29
|
Requires-Dist: pytest; extra == "dev"
|
|
28
30
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
29
31
|
Requires-Dist: pytest-mock; extra == "dev"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
fmu/__init__.py,sha256=htx6HlMme77I6pZ8U256-2B2cMJuELsu3JN3YM2Efh4,144
|
|
2
|
+
fmu/settings/__init__.py,sha256=CkEE7al_uBCQO1lxBKN5LzyCwzzH5Aq6kkEIR7f-zTw,336
|
|
3
|
+
fmu/settings/_fmu_dir.py,sha256=lmGliopDfB2QiWWAowzuaTGcN_nCYzb58-4woBAi-2g,19769
|
|
4
|
+
fmu/settings/_global_config.py,sha256=C0_o99OhOc49ynz4h6ygbbHHH8OOI5lcVFr-9FCwD0c,9331
|
|
5
|
+
fmu/settings/_init.py,sha256=rgt5aZou54RiVAOegC4kzoigJcZefPY4Kv6ke7OAIxI,4014
|
|
6
|
+
fmu/settings/_logging.py,sha256=nEdmZlNCBsB1GfDmFMKCjZmeuRp3CRlbz1EYUemc95Y,1104
|
|
7
|
+
fmu/settings/_readme_texts.py,sha256=0N3Tn3sUKc3PdTiaWzfe7fu7aAphdwMvlG_4ag3h3UE,1067
|
|
8
|
+
fmu/settings/_version.py,sha256=a3VJZDtDsD7dO22j4y92zbdkUlJwzXf_QabiVquJm1Y,706
|
|
9
|
+
fmu/settings/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
fmu/settings/types.py,sha256=aeXEsznBTT1YRRY_LSRqK1j2gmMmyLYYTGYl3a9fweU,513
|
|
11
|
+
fmu/settings/_resources/__init__.py,sha256=LHYR_F7lNGdv8N6R3cEwds5CJQpkOthXFqsEs24vgF8,118
|
|
12
|
+
fmu/settings/_resources/cache_manager.py,sha256=QxmIM0jJRgJsmD_69juPRnUmGrRJ6ZbFp6wc-KW7baA,7192
|
|
13
|
+
fmu/settings/_resources/changelog_manager.py,sha256=ubb7gqWKlAzuh3WyWSRXFFjl8Kso4amTKJPLNf4t6Mw,5857
|
|
14
|
+
fmu/settings/_resources/config_managers.py,sha256=HULkmmxKHN0_aDkPd3DrLaWv2X-njBBCQwXU-H55jdw,2334
|
|
15
|
+
fmu/settings/_resources/lock_manager.py,sha256=tqDqMhovED52a8vEcoJCbfJqgKAk2lsp5eUOtZteU00,10512
|
|
16
|
+
fmu/settings/_resources/log_manager.py,sha256=Ru7dLE6Ugnz0NYORf29kZdkrg3scV7v7z8xDiKP7wEI,3736
|
|
17
|
+
fmu/settings/_resources/pydantic_resource_manager.py,sha256=F2XDWx4RQktJSKC247J2EtJvsmT_DzZGJ9oE2Tt63Zs,14927
|
|
18
|
+
fmu/settings/_resources/user_session_log_manager.py,sha256=aGbOkjyvqKPTdKyhtNUmuWIWyRqsXIcVrNTgmq8e2yc,1610
|
|
19
|
+
fmu/settings/models/__init__.py,sha256=lRlXgl55ba2upmDzdvzx8N30JMq2Osnm8aa_xxTZn8A,112
|
|
20
|
+
fmu/settings/models/_enums.py,sha256=sBvn-2sYVToK7WBMR6PGHtdkh1MwDB0d1_6oWJMIvII,1054
|
|
21
|
+
fmu/settings/models/_mappings.py,sha256=Z4Ex7MtmajBr6FjaNzmwDRwtJlaZZ8YKh9NDmZHRKPI,2832
|
|
22
|
+
fmu/settings/models/change_info.py,sha256=5X8wIItSt_xQ3AQF8EB5moxgIDC_ZyO6Hqkxhdj9l5c,1038
|
|
23
|
+
fmu/settings/models/event_info.py,sha256=7AVDT7QEBHgKTUOHw4ofltQAY9yiYD6aYY8ilO55_9Q,429
|
|
24
|
+
fmu/settings/models/lock_info.py,sha256=-oHDF9v9bDLCoFvEg4S6XXYLeo19zRAZ8HynCv75VWg,711
|
|
25
|
+
fmu/settings/models/log.py,sha256=aP5lQTnKBIiyiTmiMXig0TEpA2yhKz6VOsymaP6EOYk,1943
|
|
26
|
+
fmu/settings/models/project_config.py,sha256=68aEugx7uwkDvVT2vAoS7F6ez8R91cvU0ULf3hwS-rE,2371
|
|
27
|
+
fmu/settings/models/user_config.py,sha256=XYxGhvdIPkQeMWBOwZKekvC-2ldwT1OhrGh9piBxVgk,2851
|
|
28
|
+
fmu_settings-0.14.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
29
|
+
fmu_settings-0.14.1.dist-info/METADATA,sha256=Zbdj0rdx5SNw1qGME_Gkdvk_wMvFUJFUyoWgHFhbRgw,2183
|
|
30
|
+
fmu_settings-0.14.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
31
|
+
fmu_settings-0.14.1.dist-info/top_level.txt,sha256=Z-FIY3pxn0UK2Wxi9IJ7fKoLSraaxuNGi1eokiE0ShM,4
|
|
32
|
+
fmu_settings-0.14.1.dist-info/RECORD,,
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
fmu/__init__.py,sha256=htx6HlMme77I6pZ8U256-2B2cMJuELsu3JN3YM2Efh4,144
|
|
2
|
-
fmu/settings/__init__.py,sha256=CkEE7al_uBCQO1lxBKN5LzyCwzzH5Aq6kkEIR7f-zTw,336
|
|
3
|
-
fmu/settings/_fmu_dir.py,sha256=XeZjec78q0IUOpBq-VMkKoWtzXwBeQi2qWRIh_SIFwU,10859
|
|
4
|
-
fmu/settings/_global_config.py,sha256=C0_o99OhOc49ynz4h6ygbbHHH8OOI5lcVFr-9FCwD0c,9331
|
|
5
|
-
fmu/settings/_init.py,sha256=ucueS0BlEsM3MkX7IaRISloH4vF7-_ZKSphrORbHgJ4,4381
|
|
6
|
-
fmu/settings/_logging.py,sha256=nEdmZlNCBsB1GfDmFMKCjZmeuRp3CRlbz1EYUemc95Y,1104
|
|
7
|
-
fmu/settings/_version.py,sha256=LGYtjQ6cyPZC_N0AovMIeSYYDK21050nm3HYgDanQBM,704
|
|
8
|
-
fmu/settings/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
fmu/settings/types.py,sha256=aeXEsznBTT1YRRY_LSRqK1j2gmMmyLYYTGYl3a9fweU,513
|
|
10
|
-
fmu/settings/_resources/__init__.py,sha256=LHYR_F7lNGdv8N6R3cEwds5CJQpkOthXFqsEs24vgF8,118
|
|
11
|
-
fmu/settings/_resources/config_managers.py,sha256=QcCLlSw8KdJKrkhGax5teFJzjgQG3ym7Ljs1DykjFbc,1570
|
|
12
|
-
fmu/settings/_resources/lock_manager.py,sha256=ekGE5mTcjEWRyoBSIeqaX1r53-WStpYeJYepkOPYi3Q,10061
|
|
13
|
-
fmu/settings/_resources/pydantic_resource_manager.py,sha256=BUWO6IHSoT0Ma6QgseweEf7uiGeMwHBEoCyGYPYYFdA,9290
|
|
14
|
-
fmu/settings/models/__init__.py,sha256=lRlXgl55ba2upmDzdvzx8N30JMq2Osnm8aa_xxTZn8A,112
|
|
15
|
-
fmu/settings/models/_enums.py,sha256=SQUZ-2mQcTx4F0oefPFfuQzMKsKTSFSB-wq_CH7TBRE,734
|
|
16
|
-
fmu/settings/models/_mappings.py,sha256=Z4Ex7MtmajBr6FjaNzmwDRwtJlaZZ8YKh9NDmZHRKPI,2832
|
|
17
|
-
fmu/settings/models/lock_info.py,sha256=-oHDF9v9bDLCoFvEg4S6XXYLeo19zRAZ8HynCv75VWg,711
|
|
18
|
-
fmu/settings/models/project_config.py,sha256=pxb54JmpXNMVAFUu_yJ89dNrYEk6hrPuFfFUpf84Jh0,1099
|
|
19
|
-
fmu/settings/models/user_config.py,sha256=dWFTcZY6UnEgNTuGqB-izraJ657PecsW0e0Nt9GBDhI,2666
|
|
20
|
-
fmu_settings-0.5.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
21
|
-
fmu_settings-0.5.2.dist-info/METADATA,sha256=dxi2pe4SgYJwTY6beiEPoLx7lEqjzZWq9UcpX3gir_s,2116
|
|
22
|
-
fmu_settings-0.5.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
23
|
-
fmu_settings-0.5.2.dist-info/top_level.txt,sha256=Z-FIY3pxn0UK2Wxi9IJ7fKoLSraaxuNGi1eokiE0ShM,4
|
|
24
|
-
fmu_settings-0.5.2.dist-info/RECORD,,
|
|
File without changes
|