fmu-settings 0.5.4__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of fmu-settings might be problematic. Click here for more details.
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/PKG-INFO +1 -1
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu/settings/_fmu_dir.py +95 -13
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu/settings/_init.py +19 -32
- fmu_settings-0.6.0/src/fmu/settings/_readme_texts.py +34 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu/settings/_resources/cache_manager.py +13 -9
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu/settings/_version.py +3 -3
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu/settings/models/project_config.py +2 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu/settings/models/user_config.py +3 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu_settings.egg-info/PKG-INFO +1 -1
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu_settings.egg-info/SOURCES.txt +1 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/tests/conftest.py +3 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/tests/test_fmu_dir.py +83 -2
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/tests/test_init.py +3 -4
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/tests/test_resources/test_cache_manager.py +9 -17
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/tests/test_resources/test_lock_manager.py +18 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/tests/test_resources/test_resource_managers.py +6 -36
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/.coveragerc +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/.github/pull_request_template.md +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/.github/workflows/ci.yml +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/.github/workflows/codeql.yml +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/.github/workflows/publish.yml +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/.gitignore +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/CONTRIBUTING.md +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/LICENSE +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/README.md +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/SECURITY.md +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/pyproject.toml +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/setup.cfg +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu/__init__.py +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu/settings/__init__.py +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu/settings/_global_config.py +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu/settings/_logging.py +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu/settings/_resources/__init__.py +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu/settings/_resources/config_managers.py +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu/settings/_resources/lock_manager.py +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu/settings/_resources/pydantic_resource_manager.py +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu/settings/models/__init__.py +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu/settings/models/_enums.py +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu/settings/models/_mappings.py +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu/settings/models/lock_info.py +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu/settings/py.typed +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu/settings/types.py +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu_settings.egg-info/dependency_links.txt +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu_settings.egg-info/requires.txt +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu_settings.egg-info/top_level.txt +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/tests/test_global_config.py +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/tests/test_resources/test_project_config.py +0 -0
- {fmu_settings-0.5.4 → fmu_settings-0.6.0}/tests/test_resources/test_user_config.py +0 -0
|
@@ -4,12 +4,13 @@ from pathlib import Path
|
|
|
4
4
|
from typing import TYPE_CHECKING, Any, Final, Self, TypeAlias, cast
|
|
5
5
|
|
|
6
6
|
from ._logging import null_logger
|
|
7
|
+
from ._readme_texts import PROJECT_README_CONTENT, USER_README_CONTENT
|
|
7
8
|
from ._resources.cache_manager import CacheManager
|
|
8
9
|
from ._resources.config_managers import (
|
|
9
10
|
ProjectConfigManager,
|
|
10
11
|
UserConfigManager,
|
|
11
12
|
)
|
|
12
|
-
from ._resources.lock_manager import LockManager
|
|
13
|
+
from ._resources.lock_manager import DEFAULT_LOCK_TIMEOUT, LockManager
|
|
13
14
|
from .models.project_config import ProjectConfig
|
|
14
15
|
from .models.user_config import UserConfig
|
|
15
16
|
|
|
@@ -24,13 +25,22 @@ class FMUDirectoryBase:
|
|
|
24
25
|
config: FMUConfigManager
|
|
25
26
|
_lock: LockManager
|
|
26
27
|
_cache_manager: CacheManager
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
_README_CONTENT: str = ""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self: Self,
|
|
32
|
+
base_path: str | Path,
|
|
33
|
+
cache_revisions: int = CacheManager.MIN_REVISIONS,
|
|
34
|
+
*,
|
|
35
|
+
lock_timeout_seconds: int = DEFAULT_LOCK_TIMEOUT,
|
|
36
|
+
) -> None:
|
|
29
37
|
"""Initializes access to a .fmu directory.
|
|
30
38
|
|
|
31
39
|
Args:
|
|
32
40
|
base_path: The directory containing the .fmu directory or one of its parent
|
|
33
41
|
dirs
|
|
42
|
+
cache_revisions: Number of revisions to retain in the cache. Minimum is 5.
|
|
43
|
+
lock_timeout_seconds: Lock expiration time in seconds. Default 20 minutes.
|
|
34
44
|
|
|
35
45
|
Raises:
|
|
36
46
|
FileExistsError: If .fmu exists but is not a directory
|
|
@@ -39,8 +49,8 @@ class FMUDirectoryBase:
|
|
|
39
49
|
"""
|
|
40
50
|
self.base_path = Path(base_path).resolve()
|
|
41
51
|
logger.debug(f"Initializing FMUDirectory from '{base_path}'")
|
|
42
|
-
self._lock = LockManager(self)
|
|
43
|
-
self._cache_manager = CacheManager(self, max_revisions=
|
|
52
|
+
self._lock = LockManager(self, timeout_seconds=lock_timeout_seconds)
|
|
53
|
+
self._cache_manager = CacheManager(self, max_revisions=cache_revisions)
|
|
44
54
|
|
|
45
55
|
fmu_dir = self.base_path / ".fmu"
|
|
46
56
|
if fmu_dir.exists():
|
|
@@ -72,8 +82,15 @@ class FMUDirectoryBase:
|
|
|
72
82
|
|
|
73
83
|
@cache_max_revisions.setter
|
|
74
84
|
def cache_max_revisions(self: Self, value: int) -> None:
|
|
75
|
-
"""Update the retention limit for revision snapshots.
|
|
76
|
-
|
|
85
|
+
"""Update the retention limit for revision snapshots.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
value: The new maximum number of revisions to retain. Minimum value is 5.
|
|
89
|
+
Values below 5 are set to 5.
|
|
90
|
+
"""
|
|
91
|
+
clamped_value = max(CacheManager.MIN_REVISIONS, value)
|
|
92
|
+
self._cache_manager.max_revisions = clamped_value
|
|
93
|
+
self.set_config_value("cache_max_revisions", clamped_value)
|
|
77
94
|
|
|
78
95
|
def get_config_value(self: Self, key: str, default: Any = None) -> Any:
|
|
79
96
|
"""Gets a configuration value by key.
|
|
@@ -230,15 +247,59 @@ class FMUDirectoryBase:
|
|
|
230
247
|
"""
|
|
231
248
|
return self.get_file_path(relative_path).exists()
|
|
232
249
|
|
|
250
|
+
def restore(self: Self) -> None:
|
|
251
|
+
"""Attempt to reconstruct missing .fmu files from in-memory state."""
|
|
252
|
+
if not self.path.exists():
|
|
253
|
+
self.path.mkdir(parents=True, exist_ok=True)
|
|
254
|
+
logger.info("Recreated missing .fmu directory at %s", self.path)
|
|
255
|
+
|
|
256
|
+
readme_path = self.get_file_path("README")
|
|
257
|
+
if self._README_CONTENT and not readme_path.exists():
|
|
258
|
+
self.write_text_file("README", self._README_CONTENT)
|
|
259
|
+
logger.info("Restored README at %s", readme_path)
|
|
260
|
+
|
|
261
|
+
config_path = self.config.path
|
|
262
|
+
if not config_path.exists():
|
|
263
|
+
cached_model = getattr(self.config, "_cache", None)
|
|
264
|
+
if cached_model is not None:
|
|
265
|
+
self.config.save(cached_model)
|
|
266
|
+
logger.info("Restored config.json from cached model at %s", config_path)
|
|
267
|
+
else:
|
|
268
|
+
self.config.reset()
|
|
269
|
+
logger.info("Restored config.json from defaults at %s", config_path)
|
|
270
|
+
|
|
233
271
|
|
|
234
272
|
class ProjectFMUDirectory(FMUDirectoryBase):
|
|
235
273
|
if TYPE_CHECKING:
|
|
236
274
|
config: ProjectConfigManager
|
|
237
275
|
|
|
238
|
-
|
|
239
|
-
|
|
276
|
+
_README_CONTENT: str = PROJECT_README_CONTENT
|
|
277
|
+
|
|
278
|
+
def __init__(
|
|
279
|
+
self: Self,
|
|
280
|
+
base_path: str | Path,
|
|
281
|
+
*,
|
|
282
|
+
lock_timeout_seconds: int = DEFAULT_LOCK_TIMEOUT,
|
|
283
|
+
) -> None:
|
|
284
|
+
"""Initializes a project-based .fmu directory.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
base_path: Project directory containing the .fmu folder.
|
|
288
|
+
lock_timeout_seconds: Lock expiration time in seconds. Default 20 minutes.
|
|
289
|
+
"""
|
|
240
290
|
self.config = ProjectConfigManager(self)
|
|
241
|
-
super().__init__(
|
|
291
|
+
super().__init__(
|
|
292
|
+
base_path,
|
|
293
|
+
CacheManager.MIN_REVISIONS,
|
|
294
|
+
lock_timeout_seconds=lock_timeout_seconds,
|
|
295
|
+
)
|
|
296
|
+
try:
|
|
297
|
+
max_revisions = self.config.get(
|
|
298
|
+
"cache_max_revisions", CacheManager.MIN_REVISIONS
|
|
299
|
+
)
|
|
300
|
+
self._cache_manager.max_revisions = max_revisions
|
|
301
|
+
except FileNotFoundError:
|
|
302
|
+
pass
|
|
242
303
|
|
|
243
304
|
def update_config(self: Self, updates: dict[str, Any]) -> ProjectConfig:
|
|
244
305
|
"""Updates multiple configuration values at once.
|
|
@@ -309,10 +370,31 @@ class UserFMUDirectory(FMUDirectoryBase):
|
|
|
309
370
|
if TYPE_CHECKING:
|
|
310
371
|
config: UserConfigManager
|
|
311
372
|
|
|
312
|
-
|
|
313
|
-
|
|
373
|
+
_README_CONTENT: str = USER_README_CONTENT
|
|
374
|
+
|
|
375
|
+
def __init__(
|
|
376
|
+
self: Self,
|
|
377
|
+
*,
|
|
378
|
+
lock_timeout_seconds: int = DEFAULT_LOCK_TIMEOUT,
|
|
379
|
+
) -> None:
|
|
380
|
+
"""Initializes a user .fmu directory.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
lock_timeout_seconds: Lock expiration time in seconds. Default 20 minutes.
|
|
384
|
+
"""
|
|
314
385
|
self.config = UserConfigManager(self)
|
|
315
|
-
super().__init__(
|
|
386
|
+
super().__init__(
|
|
387
|
+
Path.home(),
|
|
388
|
+
CacheManager.MIN_REVISIONS,
|
|
389
|
+
lock_timeout_seconds=lock_timeout_seconds,
|
|
390
|
+
)
|
|
391
|
+
try:
|
|
392
|
+
max_revisions = self.config.get(
|
|
393
|
+
"cache_max_revisions", CacheManager.MIN_REVISIONS
|
|
394
|
+
)
|
|
395
|
+
self._cache_manager.max_revisions = max_revisions
|
|
396
|
+
except FileNotFoundError:
|
|
397
|
+
pass
|
|
316
398
|
|
|
317
399
|
def update_config(self: Self, updates: dict[str, Any]) -> UserConfig:
|
|
318
400
|
"""Updates multiple configuration values at once.
|
|
@@ -1,43 +1,18 @@
|
|
|
1
1
|
"""Initializes the .fmu directory."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from textwrap import dedent
|
|
5
4
|
from typing import Any, Final
|
|
6
5
|
|
|
7
6
|
from fmu.datamodels.fmu_results.global_configuration import GlobalConfiguration
|
|
8
7
|
|
|
9
8
|
from ._fmu_dir import ProjectFMUDirectory, UserFMUDirectory
|
|
10
9
|
from ._logging import null_logger
|
|
10
|
+
from ._readme_texts import PROJECT_README_CONTENT, USER_README_CONTENT
|
|
11
|
+
from ._resources.lock_manager import DEFAULT_LOCK_TIMEOUT
|
|
11
12
|
from .models.project_config import ProjectConfig
|
|
12
13
|
|
|
13
14
|
logger: Final = null_logger(__name__)
|
|
14
15
|
|
|
15
|
-
_README = dedent("""\
|
|
16
|
-
This directory contains static configuration data for your FMU project.
|
|
17
|
-
|
|
18
|
-
You should *not* manually modify files within this directory. Doing so may
|
|
19
|
-
result in erroneous behavior or erroneous data in your FMU project.
|
|
20
|
-
|
|
21
|
-
Changes to data stored within this directory must happen through the FMU
|
|
22
|
-
Settings application.
|
|
23
|
-
|
|
24
|
-
Run `fmu-settings` to do this.
|
|
25
|
-
""")
|
|
26
|
-
|
|
27
|
-
_USER_README = dedent("""\
|
|
28
|
-
This directory contains static data and configuration elements used by some
|
|
29
|
-
components in FMU. It may also contains sensitive access tokens that should not be
|
|
30
|
-
shared with others.
|
|
31
|
-
|
|
32
|
-
You should *not* manually modify files within this directory. Doing so may
|
|
33
|
-
result in erroneous behavior by some FMU components.
|
|
34
|
-
|
|
35
|
-
Changes to data stored within this directory must happen through the FMU
|
|
36
|
-
Settings application.
|
|
37
|
-
|
|
38
|
-
Run `fmu-settings` to do this.
|
|
39
|
-
""")
|
|
40
|
-
|
|
41
16
|
|
|
42
17
|
def _create_fmu_directory(base_path: Path) -> None:
|
|
43
18
|
"""Creates the .fmu directory.
|
|
@@ -71,6 +46,8 @@ def init_fmu_directory(
|
|
|
71
46
|
base_path: str | Path,
|
|
72
47
|
config_data: ProjectConfig | dict[str, Any] | None = None,
|
|
73
48
|
global_config: GlobalConfiguration | None = None,
|
|
49
|
+
*,
|
|
50
|
+
lock_timeout_seconds: int = DEFAULT_LOCK_TIMEOUT,
|
|
74
51
|
) -> ProjectFMUDirectory:
|
|
75
52
|
"""Creates and initializes a .fmu directory.
|
|
76
53
|
|
|
@@ -83,6 +60,7 @@ def init_fmu_directory(
|
|
|
83
60
|
data.
|
|
84
61
|
global_config: Optional GlobaConfiguration instance with existing global config
|
|
85
62
|
data.
|
|
63
|
+
lock_timeout_seconds: Lock expiration time in seconds. Default 20 minutes.
|
|
86
64
|
|
|
87
65
|
Returns:
|
|
88
66
|
Instance of FMUDirectory
|
|
@@ -98,8 +76,11 @@ def init_fmu_directory(
|
|
|
98
76
|
|
|
99
77
|
_create_fmu_directory(base_path)
|
|
100
78
|
|
|
101
|
-
fmu_dir = ProjectFMUDirectory(
|
|
102
|
-
|
|
79
|
+
fmu_dir = ProjectFMUDirectory(
|
|
80
|
+
base_path,
|
|
81
|
+
lock_timeout_seconds=lock_timeout_seconds,
|
|
82
|
+
)
|
|
83
|
+
fmu_dir.write_text_file("README", PROJECT_README_CONTENT)
|
|
103
84
|
|
|
104
85
|
fmu_dir.config.reset()
|
|
105
86
|
if config_data:
|
|
@@ -115,9 +96,15 @@ def init_fmu_directory(
|
|
|
115
96
|
return fmu_dir
|
|
116
97
|
|
|
117
98
|
|
|
118
|
-
def init_user_fmu_directory(
|
|
99
|
+
def init_user_fmu_directory(
|
|
100
|
+
*,
|
|
101
|
+
lock_timeout_seconds: int = DEFAULT_LOCK_TIMEOUT,
|
|
102
|
+
) -> UserFMUDirectory:
|
|
119
103
|
"""Creates and initializes a user's $HOME/.fmu directory.
|
|
120
104
|
|
|
105
|
+
Args:
|
|
106
|
+
lock_timeout_seconds: Lock expiration time in seconds. Default 20 minutes.
|
|
107
|
+
|
|
121
108
|
Returns:
|
|
122
109
|
Instance of FMUDirectory
|
|
123
110
|
|
|
@@ -131,8 +118,8 @@ def init_user_fmu_directory() -> UserFMUDirectory:
|
|
|
131
118
|
|
|
132
119
|
_create_fmu_directory(Path.home())
|
|
133
120
|
|
|
134
|
-
fmu_dir = UserFMUDirectory()
|
|
135
|
-
fmu_dir.write_text_file("README",
|
|
121
|
+
fmu_dir = UserFMUDirectory(lock_timeout_seconds=lock_timeout_seconds)
|
|
122
|
+
fmu_dir.write_text_file("README", USER_README_CONTENT)
|
|
136
123
|
|
|
137
124
|
fmu_dir.config.reset()
|
|
138
125
|
logger.debug(f"Successfully initialized .fmu directory at '{fmu_dir}'")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Shared README content for .fmu directories."""
|
|
2
|
+
|
|
3
|
+
from textwrap import dedent
|
|
4
|
+
from typing import Final
|
|
5
|
+
|
|
6
|
+
PROJECT_README_CONTENT: Final[str] = dedent(
|
|
7
|
+
"""\
|
|
8
|
+
This directory contains static configuration data for your FMU project.
|
|
9
|
+
|
|
10
|
+
You should *not* manually modify files within this directory. Doing so may
|
|
11
|
+
result in erroneous behavior or erroneous data in your FMU project.
|
|
12
|
+
|
|
13
|
+
Changes to data stored within this directory must happen through the FMU
|
|
14
|
+
Settings application.
|
|
15
|
+
|
|
16
|
+
Run `fmu-settings` to do this.
|
|
17
|
+
"""
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
USER_README_CONTENT: Final[str] = dedent(
|
|
21
|
+
"""\
|
|
22
|
+
This directory contains static data and configuration elements used by some
|
|
23
|
+
components in FMU. It may also contains sensitive access tokens that should not be
|
|
24
|
+
shared with others.
|
|
25
|
+
|
|
26
|
+
You should *not* manually modify files within this directory. Doing so may
|
|
27
|
+
result in erroneous behavior by some FMU components.
|
|
28
|
+
|
|
29
|
+
Changes to data stored within this directory must happen through the FMU
|
|
30
|
+
Settings application.
|
|
31
|
+
|
|
32
|
+
Run `fmu-settings` to do this.
|
|
33
|
+
"""
|
|
34
|
+
)
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from datetime import UTC, datetime
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import TYPE_CHECKING, Final, Self
|
|
7
|
+
from typing import TYPE_CHECKING, ClassVar, Final, Self
|
|
8
8
|
from uuid import uuid4
|
|
9
9
|
|
|
10
10
|
from fmu.settings._logging import null_logger
|
|
@@ -25,6 +25,8 @@ _CACHEDIR_TAG_CONTENT: Final = (
|
|
|
25
25
|
class CacheManager:
|
|
26
26
|
"""Stores complete file revisions under the `.fmu/cache` tree."""
|
|
27
27
|
|
|
28
|
+
MIN_REVISIONS: ClassVar[int] = 5
|
|
29
|
+
|
|
28
30
|
def __init__(
|
|
29
31
|
self: Self,
|
|
30
32
|
fmu_dir: FMUDirectoryBase,
|
|
@@ -35,10 +37,11 @@ class CacheManager:
|
|
|
35
37
|
Args:
|
|
36
38
|
fmu_dir: The FMUDirectory instance.
|
|
37
39
|
max_revisions: Maximum number of revisions to retain. Default is 5.
|
|
40
|
+
Values below 5 are set to 5.
|
|
38
41
|
"""
|
|
39
42
|
self._fmu_dir = fmu_dir
|
|
40
43
|
self._cache_root = Path("cache")
|
|
41
|
-
self._max_revisions = max(
|
|
44
|
+
self._max_revisions = max(self.MIN_REVISIONS, max_revisions)
|
|
42
45
|
|
|
43
46
|
@property
|
|
44
47
|
def max_revisions(self: Self) -> int:
|
|
@@ -47,8 +50,13 @@ class CacheManager:
|
|
|
47
50
|
|
|
48
51
|
@max_revisions.setter
|
|
49
52
|
def max_revisions(self: Self, value: int) -> None:
|
|
50
|
-
"""Update the per-resource revision retention.
|
|
51
|
-
|
|
53
|
+
"""Update the per-resource revision retention.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
value: The new maximum number of revisions. Minimum value is 5.
|
|
57
|
+
Values below 5 are set to 5.
|
|
58
|
+
"""
|
|
59
|
+
self._max_revisions = max(self.MIN_REVISIONS, value)
|
|
52
60
|
|
|
53
61
|
def store_revision(
|
|
54
62
|
self: Self,
|
|
@@ -65,12 +73,8 @@ class CacheManager:
|
|
|
65
73
|
encoding: Encoding used when persisting the snapshot. Defaults to UTF-8.
|
|
66
74
|
|
|
67
75
|
Returns:
|
|
68
|
-
Absolute filesystem path to the stored snapshot
|
|
69
|
-
disabled (``max_revisions`` equals zero).
|
|
76
|
+
Absolute filesystem path to the stored snapshot.
|
|
70
77
|
"""
|
|
71
|
-
if self.max_revisions == 0:
|
|
72
|
-
return None
|
|
73
|
-
|
|
74
78
|
resource_file_path = Path(resource_file_path)
|
|
75
79
|
cache_dir = self._ensure_resource_cache_dir(resource_file_path)
|
|
76
80
|
snapshot_name = self._snapshot_filename(resource_file_path)
|
|
@@ -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.6.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 6, 0)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'g746436036'
|
|
@@ -23,6 +23,7 @@ class ProjectConfig(ResettableBaseModel):
|
|
|
23
23
|
masterdata: Masterdata | None = Field(default=None)
|
|
24
24
|
model: Model | None = Field(default=None)
|
|
25
25
|
access: Access | None = Field(default=None)
|
|
26
|
+
cache_max_revisions: int = Field(default=5, ge=5)
|
|
26
27
|
|
|
27
28
|
@classmethod
|
|
28
29
|
def reset(cls: type[Self]) -> Self:
|
|
@@ -38,4 +39,5 @@ class ProjectConfig(ResettableBaseModel):
|
|
|
38
39
|
masterdata=None,
|
|
39
40
|
model=None,
|
|
40
41
|
access=None,
|
|
42
|
+
cache_max_revisions=5,
|
|
41
43
|
)
|
|
@@ -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,7 @@ class UserConfig(ResettableBaseModel):
|
|
|
42
43
|
|
|
43
44
|
version: VersionStr
|
|
44
45
|
created_at: AwareDatetime
|
|
46
|
+
cache_max_revisions: int = Field(default=5, ge=5)
|
|
45
47
|
user_api_keys: UserAPIKeys
|
|
46
48
|
recent_project_directories: RecentProjectDirectories
|
|
47
49
|
|
|
@@ -51,6 +53,7 @@ class UserConfig(ResettableBaseModel):
|
|
|
51
53
|
return cls(
|
|
52
54
|
version=__version__,
|
|
53
55
|
created_at=datetime.now(UTC),
|
|
56
|
+
cache_max_revisions=5,
|
|
54
57
|
user_api_keys=UserAPIKeys(),
|
|
55
58
|
recent_project_directories=[],
|
|
56
59
|
)
|
|
@@ -39,6 +39,7 @@ def config_dict(unix_epoch_utc: datetime) -> dict[str, Any]:
|
|
|
39
39
|
"version": __version__,
|
|
40
40
|
"created_at": unix_epoch_utc,
|
|
41
41
|
"created_by": "user",
|
|
42
|
+
"cache_max_revisions": 5,
|
|
42
43
|
"masterdata": None,
|
|
43
44
|
"model": None,
|
|
44
45
|
"access": None,
|
|
@@ -283,6 +284,7 @@ def config_dict_with_masterdata(
|
|
|
283
284
|
"version": __version__,
|
|
284
285
|
"created_at": unix_epoch_utc,
|
|
285
286
|
"created_by": "user",
|
|
287
|
+
"cache_max_revisions": 5,
|
|
286
288
|
"masterdata": masterdata_dict,
|
|
287
289
|
"model": model_dict,
|
|
288
290
|
}
|
|
@@ -308,6 +310,7 @@ def user_config_dict(unix_epoch_utc: datetime) -> dict[str, Any]:
|
|
|
308
310
|
return {
|
|
309
311
|
"version": __version__,
|
|
310
312
|
"created_at": unix_epoch_utc,
|
|
313
|
+
"cache_max_revisions": 5,
|
|
311
314
|
"user_api_keys": {
|
|
312
315
|
"smda_subscription": None,
|
|
313
316
|
},
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""Tests for the ProjectFMUDirectory class."""
|
|
2
2
|
|
|
3
|
+
import inspect
|
|
3
4
|
import json
|
|
5
|
+
import shutil
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
from unittest.mock import patch
|
|
6
8
|
|
|
@@ -8,8 +10,13 @@ import pytest
|
|
|
8
10
|
from pytest import MonkeyPatch
|
|
9
11
|
|
|
10
12
|
from fmu.settings import __version__, find_nearest_fmu_directory, get_fmu_directory
|
|
11
|
-
from fmu.settings._fmu_dir import
|
|
12
|
-
|
|
13
|
+
from fmu.settings._fmu_dir import (
|
|
14
|
+
FMUDirectoryBase,
|
|
15
|
+
ProjectFMUDirectory,
|
|
16
|
+
UserFMUDirectory,
|
|
17
|
+
)
|
|
18
|
+
from fmu.settings._readme_texts import PROJECT_README_CONTENT, USER_README_CONTENT
|
|
19
|
+
from fmu.settings._resources.lock_manager import DEFAULT_LOCK_TIMEOUT, LockManager
|
|
13
20
|
|
|
14
21
|
|
|
15
22
|
def test_init_existing_directory(fmu_dir: ProjectFMUDirectory) -> None:
|
|
@@ -390,3 +397,77 @@ def test_acquire_lock_on_user_fmu(
|
|
|
390
397
|
assert user_fmu_dir._lock.is_acquired()
|
|
391
398
|
assert user_fmu_dir._lock.exists
|
|
392
399
|
assert (user_fmu_dir.path / ".lock").exists()
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def test_restore_rebuilds_project_fmu_from_cache(
|
|
403
|
+
fmu_dir: ProjectFMUDirectory,
|
|
404
|
+
) -> None:
|
|
405
|
+
"""Tests that restore should recreate missing files using cached config data."""
|
|
406
|
+
fmu_dir.update_config({"version": "123.4.5"})
|
|
407
|
+
cached_dump = json.loads((fmu_dir.path / "config.json").read_text())
|
|
408
|
+
|
|
409
|
+
shutil.rmtree(fmu_dir.path)
|
|
410
|
+
assert not fmu_dir.path.exists()
|
|
411
|
+
|
|
412
|
+
fmu_dir.restore()
|
|
413
|
+
|
|
414
|
+
assert fmu_dir.path.exists()
|
|
415
|
+
readme_path = fmu_dir.path / "README"
|
|
416
|
+
assert readme_path.exists()
|
|
417
|
+
assert readme_path.read_text() == PROJECT_README_CONTENT
|
|
418
|
+
|
|
419
|
+
restored_dump = json.loads((fmu_dir.path / "config.json").read_text())
|
|
420
|
+
assert restored_dump == cached_dump
|
|
421
|
+
|
|
422
|
+
cache_dir = fmu_dir.path / "cache" / "config"
|
|
423
|
+
assert cache_dir.is_dir()
|
|
424
|
+
assert any(cache_dir.iterdir())
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def test_restore_resets_when_cache_missing(
|
|
428
|
+
fmu_dir: ProjectFMUDirectory,
|
|
429
|
+
) -> None:
|
|
430
|
+
"""Tests that restore should fall back to reset when no cached config exists."""
|
|
431
|
+
fmu_dir.config._cache = None
|
|
432
|
+
shutil.rmtree(fmu_dir.path)
|
|
433
|
+
assert not fmu_dir.path.exists()
|
|
434
|
+
|
|
435
|
+
with patch.object(
|
|
436
|
+
fmu_dir.config, "reset", wraps=fmu_dir.config.reset
|
|
437
|
+
) as mock_reset:
|
|
438
|
+
fmu_dir.restore()
|
|
439
|
+
|
|
440
|
+
mock_reset.assert_called_once()
|
|
441
|
+
assert fmu_dir.path.exists()
|
|
442
|
+
readme_path = fmu_dir.path / "README"
|
|
443
|
+
assert readme_path.exists()
|
|
444
|
+
assert readme_path.read_text() == PROJECT_README_CONTENT
|
|
445
|
+
assert (fmu_dir.config.path).exists()
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def test_restore_rebuilds_user_fmu(user_fmu_dir: UserFMUDirectory) -> None:
|
|
449
|
+
"""Tests that user FMU restore should recreate missing files using cached state."""
|
|
450
|
+
cached_dump = json.loads((user_fmu_dir.path / "config.json").read_text())
|
|
451
|
+
|
|
452
|
+
shutil.rmtree(user_fmu_dir.path)
|
|
453
|
+
assert not user_fmu_dir.path.exists()
|
|
454
|
+
|
|
455
|
+
user_fmu_dir.restore()
|
|
456
|
+
|
|
457
|
+
assert user_fmu_dir.path.exists()
|
|
458
|
+
readme_path = user_fmu_dir.path / "README"
|
|
459
|
+
assert readme_path.exists()
|
|
460
|
+
assert readme_path.read_text() == USER_README_CONTENT
|
|
461
|
+
|
|
462
|
+
restored_dump = json.loads((user_fmu_dir.path / "config.json").read_text())
|
|
463
|
+
assert restored_dump == cached_dump
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def test_fmu_directory_base_exposes_lock_timeout_kwarg() -> None:
|
|
467
|
+
"""Tests that the kw-only lock timeout argument remains available."""
|
|
468
|
+
signature = inspect.signature(FMUDirectoryBase.__init__)
|
|
469
|
+
lock_timeout = signature.parameters.get("lock_timeout_seconds")
|
|
470
|
+
|
|
471
|
+
assert lock_timeout is not None, "lock_timeout_seconds kwarg missing from base init"
|
|
472
|
+
assert lock_timeout.kind is inspect.Parameter.KEYWORD_ONLY
|
|
473
|
+
assert lock_timeout.default == DEFAULT_LOCK_TIMEOUT
|
|
@@ -11,12 +11,11 @@ import pytest
|
|
|
11
11
|
from fmu.settings import __version__
|
|
12
12
|
from fmu.settings._global_config import find_global_config
|
|
13
13
|
from fmu.settings._init import (
|
|
14
|
-
_README,
|
|
15
|
-
_USER_README,
|
|
16
14
|
_create_fmu_directory,
|
|
17
15
|
init_fmu_directory,
|
|
18
16
|
init_user_fmu_directory,
|
|
19
17
|
)
|
|
18
|
+
from fmu.settings._readme_texts import PROJECT_README_CONTENT, USER_README_CONTENT
|
|
20
19
|
from fmu.settings.models.project_config import ProjectConfig
|
|
21
20
|
from fmu.settings.models.user_config import UserConfig
|
|
22
21
|
|
|
@@ -128,7 +127,7 @@ def test_readme_is_written(tmp_path: Path, config_model: ProjectConfig) -> None:
|
|
|
128
127
|
|
|
129
128
|
readme = fmu_dir.path / "README"
|
|
130
129
|
assert readme.exists()
|
|
131
|
-
assert readme.read_text() ==
|
|
130
|
+
assert readme.read_text() == PROJECT_README_CONTENT
|
|
132
131
|
|
|
133
132
|
|
|
134
133
|
def test_init_user_fmu_directory(
|
|
@@ -178,4 +177,4 @@ def test_user_readme_is_written(tmp_path: Path, config_model: ProjectConfig) ->
|
|
|
178
177
|
|
|
179
178
|
readme = fmu_dir.path / "README"
|
|
180
179
|
assert readme.exists()
|
|
181
|
-
assert readme.read_text() ==
|
|
180
|
+
assert readme.read_text() == USER_README_CONTENT
|
|
@@ -5,7 +5,10 @@ from __future__ import annotations
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
|
-
from fmu.settings._resources.cache_manager import
|
|
8
|
+
from fmu.settings._resources.cache_manager import (
|
|
9
|
+
_CACHEDIR_TAG_CONTENT,
|
|
10
|
+
CacheManager,
|
|
11
|
+
)
|
|
9
12
|
|
|
10
13
|
if TYPE_CHECKING:
|
|
11
14
|
import pytest
|
|
@@ -84,8 +87,9 @@ def test_cache_manager_trim_handles_missing_files(
|
|
|
84
87
|
monkeypatch: pytest.MonkeyPatch,
|
|
85
88
|
) -> None:
|
|
86
89
|
"""Trimming gracefully handles concurrent removals."""
|
|
87
|
-
manager = CacheManager(fmu_dir, max_revisions=
|
|
88
|
-
|
|
90
|
+
manager = CacheManager(fmu_dir, max_revisions=CacheManager.MIN_REVISIONS)
|
|
91
|
+
for i in range(CacheManager.MIN_REVISIONS + 2):
|
|
92
|
+
manager.store_revision("foo.json", f"content_{i}")
|
|
89
93
|
|
|
90
94
|
original_unlink = Path.unlink
|
|
91
95
|
|
|
@@ -98,19 +102,7 @@ def test_cache_manager_trim_handles_missing_files(
|
|
|
98
102
|
|
|
99
103
|
monkeypatch.setattr(Path, "unlink", flaky_unlink)
|
|
100
104
|
|
|
101
|
-
manager.store_revision("foo.json", "
|
|
105
|
+
manager.store_revision("foo.json", "final")
|
|
102
106
|
|
|
103
107
|
config_cache = fmu_dir.path / "cache" / "foo"
|
|
104
|
-
assert
|
|
105
|
-
assert len(_read_snapshot_names(config_cache)) == 1
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def test_cache_manager_max_revisions_zero_skips_storage(
|
|
109
|
-
fmu_dir: ProjectFMUDirectory,
|
|
110
|
-
) -> None:
|
|
111
|
-
"""Storing with zero retention should return None and create nothing."""
|
|
112
|
-
manager = CacheManager(fmu_dir, max_revisions=0)
|
|
113
|
-
result = manager.store_revision("foo.json", "data")
|
|
114
|
-
assert result is None
|
|
115
|
-
cache_dir = fmu_dir.path / "cache" / "foo"
|
|
116
|
-
assert not cache_dir.exists()
|
|
108
|
+
assert len(_read_snapshot_names(config_cache)) == CacheManager.MIN_REVISIONS
|
|
@@ -523,6 +523,24 @@ def test_refresh_without_lock_file(
|
|
|
523
523
|
assert fmu_dir._lock.is_acquired() is False
|
|
524
524
|
|
|
525
525
|
|
|
526
|
+
def test_refresh_missing_lock_releases_owned_lock(
|
|
527
|
+
fmu_dir: ProjectFMUDirectory,
|
|
528
|
+
) -> None:
|
|
529
|
+
"""Tests refresh releases cached state when lock file is missing."""
|
|
530
|
+
lock = LockManager(fmu_dir)
|
|
531
|
+
lock.acquire()
|
|
532
|
+
lock.path.unlink()
|
|
533
|
+
|
|
534
|
+
with (
|
|
535
|
+
patch.object(lock, "is_acquired", return_value=True),
|
|
536
|
+
patch.object(lock, "release") as mock_release,
|
|
537
|
+
pytest.raises(LockNotFoundError, match="lock file does not exist"),
|
|
538
|
+
):
|
|
539
|
+
lock.refresh()
|
|
540
|
+
|
|
541
|
+
mock_release.assert_called_once()
|
|
542
|
+
|
|
543
|
+
|
|
526
544
|
def test_refresh_without_owning_lock(
|
|
527
545
|
fmu_dir: ProjectFMUDirectory, monkeypatch: MonkeyPatch
|
|
528
546
|
) -> None:
|
|
@@ -10,7 +10,6 @@ import pytest
|
|
|
10
10
|
from pydantic import BaseModel
|
|
11
11
|
|
|
12
12
|
from fmu.settings._fmu_dir import ProjectFMUDirectory
|
|
13
|
-
from fmu.settings._resources.cache_manager import CacheManager
|
|
14
13
|
from fmu.settings._resources.lock_manager import LockManager
|
|
15
14
|
from fmu.settings._resources.pydantic_resource_manager import PydanticResourceManager
|
|
16
15
|
|
|
@@ -230,57 +229,28 @@ def test_pydantic_resource_manager_save_stores_revision_when_enabled(
|
|
|
230
229
|
|
|
231
230
|
|
|
232
231
|
def test_pydantic_resource_manager_revision_cache_trims_excess(
|
|
233
|
-
fmu_dir: ProjectFMUDirectory, monkeypatch: pytest.MonkeyPatch
|
|
234
|
-
) -> None:
|
|
235
|
-
"""Revision caching should retain only the configured number of snapshots."""
|
|
236
|
-
original_limit = fmu_dir.cache_max_revisions
|
|
237
|
-
fmu_dir.cache_max_revisions = 2
|
|
238
|
-
try:
|
|
239
|
-
sequence = iter(["rev1.json", "rev2.json", "rev3.json"])
|
|
240
|
-
monkeypatch.setattr(
|
|
241
|
-
CacheManager,
|
|
242
|
-
"_snapshot_filename",
|
|
243
|
-
lambda self, config_file_path: next(sequence),
|
|
244
|
-
)
|
|
245
|
-
|
|
246
|
-
a = AManager(fmu_dir)
|
|
247
|
-
a.save(A(foo="one"))
|
|
248
|
-
a.save(A(foo="two"))
|
|
249
|
-
a.save(A(foo="three"))
|
|
250
|
-
finally:
|
|
251
|
-
fmu_dir.cache_max_revisions = original_limit
|
|
252
|
-
|
|
253
|
-
config_cache = fmu_dir.path / "cache" / "foo"
|
|
254
|
-
snapshots = sorted(p.name for p in config_cache.iterdir())
|
|
255
|
-
assert snapshots == ["rev2.json", "rev3.json"]
|
|
256
|
-
|
|
257
|
-
assert (
|
|
258
|
-
json.loads((config_cache / "rev3.json").read_text(encoding="utf-8"))["foo"]
|
|
259
|
-
== "three"
|
|
260
|
-
)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
def test_pydantic_resource_manager_respects_retention_setting(
|
|
264
232
|
fmu_dir: ProjectFMUDirectory,
|
|
265
233
|
) -> None:
|
|
266
|
-
"""
|
|
234
|
+
"""Revision caching should retain only the configured number of snapshots."""
|
|
267
235
|
original_limit = fmu_dir.cache_max_revisions
|
|
268
|
-
fmu_dir.cache_max_revisions =
|
|
236
|
+
fmu_dir.cache_max_revisions = 5
|
|
269
237
|
try:
|
|
270
238
|
a = AManager(fmu_dir)
|
|
271
239
|
a.save(A(foo="one"))
|
|
272
240
|
a.save(A(foo="two"))
|
|
273
241
|
a.save(A(foo="three"))
|
|
274
242
|
a.save(A(foo="four"))
|
|
243
|
+
a.save(A(foo="five"))
|
|
244
|
+
a.save(A(foo="six"))
|
|
275
245
|
finally:
|
|
276
246
|
fmu_dir.cache_max_revisions = original_limit
|
|
277
247
|
|
|
278
248
|
config_cache = fmu_dir.path / "cache" / "foo"
|
|
279
249
|
snapshots = sorted(p.name for p in config_cache.iterdir())
|
|
280
|
-
assert len(snapshots) ==
|
|
250
|
+
assert len(snapshots) == 5 # noqa: PLR2004
|
|
281
251
|
|
|
282
252
|
contents = [
|
|
283
253
|
json.loads((config_cache / name).read_text(encoding="utf-8"))["foo"]
|
|
284
254
|
for name in snapshots
|
|
285
255
|
]
|
|
286
|
-
assert contents == ["two", "three", "four"]
|
|
256
|
+
assert contents == ["two", "three", "four", "five", "six"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fmu_settings-0.5.4 → fmu_settings-0.6.0}/src/fmu/settings/_resources/pydantic_resource_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|