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 ADDED
@@ -0,0 +1,6 @@
1
+ """The fmu.* package namespace.
2
+
3
+ Do not add anything else to this module.
4
+ """
5
+
6
+ __path__ = __import__("pkgutil").extend_path(__path__, __name__)
@@ -0,0 +1,12 @@
1
+ """The fmu-settings package."""
2
+
3
+ try:
4
+ from ._version import version
5
+
6
+ __version__ = version
7
+ except ImportError:
8
+ __version__ = version = "0.0.0"
9
+
10
+ from ._fmu_dir import ProjectFMUDirectory, find_nearest_fmu_directory, get_fmu_directory
11
+
12
+ __all__ = ["get_fmu_directory", "ProjectFMUDirectory", "find_nearest_fmu_directory"]
@@ -0,0 +1,337 @@
1
+ """Main interface for working with .fmu directory."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any, Final, Self, TypeAlias, cast
5
+
6
+ from ._logging import null_logger
7
+ from .models.project_config import ProjectConfig
8
+ from .models.user_config import UserConfig
9
+ from .resources.config_managers import (
10
+ ProjectConfigManager,
11
+ UserConfigManager,
12
+ )
13
+
14
+ logger: Final = null_logger(__name__)
15
+
16
+ FMUConfigManager: TypeAlias = ProjectConfigManager | UserConfigManager
17
+
18
+
19
+ class FMUDirectoryBase:
20
+ """Provides access to a .fmu directory and operations on its contents."""
21
+
22
+ config: FMUConfigManager
23
+
24
+ def __init__(self: Self, base_path: str | Path) -> None:
25
+ """Initializes access to a .fmu directory.
26
+
27
+ Args:
28
+ base_path: The directory containing the .fmu directory or one of its parent
29
+ dirs
30
+
31
+ Raises:
32
+ FileExistsError: If .fmu exists but is not a directory
33
+ FileNotFoundError: If .fmu directory doesn't exist
34
+ PermissionError: If lacking permissions to read/write to the directory
35
+ """
36
+ self.base_path = Path(base_path).resolve()
37
+ logger.debug(f"Initializing FMUDirectory from '{base_path}'")
38
+
39
+ fmu_dir = self.base_path / ".fmu"
40
+ if fmu_dir.exists():
41
+ if fmu_dir.is_dir():
42
+ self._path = fmu_dir
43
+ else:
44
+ raise FileExistsError(
45
+ f".fmu exists at {self.base_path} but is not a directory"
46
+ )
47
+ else:
48
+ raise FileNotFoundError(f"No .fmu directory found at {self.base_path}")
49
+
50
+ logger.debug(f"Using .fmu directory at {self._path}")
51
+
52
+ @property
53
+ def path(self: Self) -> Path:
54
+ """Returns the path to the .fmu directory."""
55
+ return self._path
56
+
57
+ def get_config_value(self: Self, key: str, default: Any = None) -> Any:
58
+ """Gets a configuration value by key.
59
+
60
+ Supports dot notation for nested values (e.g., "foo.bar")
61
+
62
+ Args:
63
+ key: The configuration key
64
+ default: Value to return if key is not found. Default None
65
+
66
+ Returns:
67
+ The configuration value or deafult
68
+ """
69
+ return self.config.get(key, default)
70
+
71
+ def set_config_value(self: Self, key: str, value: Any) -> None:
72
+ """Sets a configuration value by key.
73
+
74
+ Args:
75
+ key: The configuration key
76
+ value: The value to set
77
+
78
+ Raises:
79
+ FileNotFoundError: If config file doesn't exist
80
+ ValueError: If the updated config is invalid
81
+ """
82
+ self.config.set(key, value)
83
+
84
+ def update_config(
85
+ self: Self, updates: dict[str, Any]
86
+ ) -> ProjectConfig | UserConfig:
87
+ """Updates multiple configuration values at once.
88
+
89
+ Args:
90
+ updates: Dictionary of key-value pairs to update
91
+
92
+ Returns:
93
+ The updated *Config object
94
+
95
+ Raises:
96
+ FileNotFoundError: If config file doesn't exist
97
+ ValueError: If the updates config is invalid
98
+ """
99
+ return self.config.update(updates)
100
+
101
+ def get_file_path(self: Self, relative_path: str | Path) -> Path:
102
+ """Gets the absolute path to a file within the .fmu directory.
103
+
104
+ Args:
105
+ relative_path: Path relative to the .fmu directory
106
+
107
+ Returns:
108
+ Absolute path to the file
109
+ """
110
+ return self.path / relative_path
111
+
112
+ def read_file(self, relative_path: str | Path) -> bytes:
113
+ """Reads a file from the .fmu directory.
114
+
115
+ Args:
116
+ relative_path: Path relative to the .fmu directory
117
+
118
+ Returns:
119
+ File contents as bytes
120
+
121
+ Raises:
122
+ FileNotFoundError: If the file doesn't exist
123
+ """
124
+ file_path = self.get_file_path(relative_path)
125
+ return file_path.read_bytes()
126
+
127
+ def read_text_file(self, relative_path: str | Path, encoding: str = "utf-8") -> str:
128
+ """Reads a text file from the .fmu directory.
129
+
130
+ Args:
131
+ relative_path: Path relative to the .fmu directory
132
+ encoding: Text encoding to use. Default utf-8
133
+
134
+ Returns:
135
+ File contents as string
136
+ """
137
+ file_path = self.get_file_path(relative_path)
138
+ return file_path.read_text(encoding=encoding)
139
+
140
+ def write_file(self, relative_path: str | Path, data: bytes) -> None:
141
+ """Writes bytes to a file in the .fmu directory.
142
+
143
+ Args:
144
+ relative_path: Path relative to the .fmu directory
145
+ data: Bytes to write
146
+ """
147
+ file_path = self.get_file_path(relative_path)
148
+ file_path.parent.mkdir(parents=True, exist_ok=True)
149
+
150
+ file_path.write_bytes(data)
151
+ logger.debug(f"Wrote {len(data)} bytes to {file_path}")
152
+
153
+ def write_text_file(
154
+ self, relative_path: str | Path, content: str, encoding: str = "utf-8"
155
+ ) -> None:
156
+ """Writes text to a file in the .fmu directory.
157
+
158
+ Args:
159
+ relative_path: Path relative to the .fmu directory
160
+ content: Text content to write
161
+ encoding: Text encoding to use. Default utf-8
162
+ """
163
+ file_path = self.get_file_path(relative_path)
164
+ file_path.parent.mkdir(parents=True, exist_ok=True)
165
+
166
+ file_path.write_text(content, encoding=encoding)
167
+ logger.debug(f"Wrote text file to {file_path}")
168
+
169
+ def list_files(self, subdirectory: str | Path | None = None) -> list[Path]:
170
+ """Lists files in the .fmu directory or a subdirectory.
171
+
172
+ Args:
173
+ subdirectory: Optional subdirectory to list files from
174
+
175
+ Returns:
176
+ List of Path objects for files (not directories)
177
+ """
178
+ base = self.get_file_path(subdirectory) if subdirectory else self.path
179
+ if not base.exists():
180
+ return []
181
+
182
+ return [p for p in base.iterdir() if p.is_file()]
183
+
184
+ def ensure_directory(self, relative_path: str | Path) -> Path:
185
+ """Ensures a subdirectory exists in the .fmu directory.
186
+
187
+ Args:
188
+ relative_path: Path relative to the .fmu directory
189
+
190
+ Returns:
191
+ Path to the directory
192
+ """
193
+ dir_path = self.get_file_path(relative_path)
194
+ dir_path.mkdir(parents=True, exist_ok=True)
195
+ return dir_path
196
+
197
+ def file_exists(self, relative_path: str | Path) -> bool:
198
+ """Checks if a file exists in the .fmu directory.
199
+
200
+ Args:
201
+ relative_path: Path relative to the .fmu directory
202
+
203
+ Returns:
204
+ True if the file exists, False otherwise
205
+ """
206
+ return self.get_file_path(relative_path).exists()
207
+
208
+
209
+ class ProjectFMUDirectory(FMUDirectoryBase):
210
+ config: ProjectConfigManager
211
+
212
+ def __init__(self, base_path: str | Path) -> None:
213
+ """Initializes a project-based .fmu directory."""
214
+ self.config = ProjectConfigManager(self)
215
+ super().__init__(base_path)
216
+
217
+ def update_config(self: Self, updates: dict[str, Any]) -> ProjectConfig:
218
+ """Updates multiple configuration values at once.
219
+
220
+ Args:
221
+ updates: Dictionary of key-value pairs to update
222
+
223
+ Returns:
224
+ The updated ProjectConfig object
225
+
226
+ Raises:
227
+ FileNotFoundError: If config file doesn't exist
228
+ ValueError: If the updates config is invalid
229
+ """
230
+ return cast("ProjectConfig", super().update_config(updates))
231
+
232
+ @staticmethod
233
+ def find_fmu_directory(start_path: Path) -> Path | None:
234
+ """Searches for a .fmu directory in start_path and its parents.
235
+
236
+ Args:
237
+ start_path: The path to start searching from
238
+
239
+ Returns:
240
+ Path to the found .fmu directory or None if not found
241
+ """
242
+ current = start_path
243
+ # Prevent symlink loops
244
+ visited = set()
245
+
246
+ while current not in visited:
247
+ visited.add(current)
248
+ fmu_dir = current / ".fmu"
249
+
250
+ # Do not include $HOME/.fmu in the search
251
+ if fmu_dir.is_dir() and current != Path.home():
252
+ return fmu_dir
253
+
254
+ # We hit root
255
+ if current == current.parent:
256
+ break
257
+
258
+ current = current.parent
259
+
260
+ return None
261
+
262
+ @classmethod
263
+ def find_nearest(cls: type[Self], start_path: str | Path = ".") -> Self:
264
+ """Factory method to find and open the nearest .fmu directory.
265
+
266
+ Args:
267
+ start_path: Path to start searching from. Default current working director
268
+
269
+ Returns:
270
+ FMUDirectory instance
271
+
272
+ Raises:
273
+ FileNotFoundError: If no .fmu directory is found
274
+ """
275
+ start_path = Path(start_path).resolve()
276
+ fmu_dir_path = cls.find_fmu_directory(start_path)
277
+ if fmu_dir_path is None:
278
+ raise FileNotFoundError(f"No .fmu directory found at or above {start_path}")
279
+ return cls(fmu_dir_path.parent)
280
+
281
+
282
+ class UserFMUDirectory(FMUDirectoryBase):
283
+ config: UserConfigManager
284
+
285
+ def __init__(self) -> None:
286
+ """Initializes a project-based .fmu directory."""
287
+ self.config = UserConfigManager(self)
288
+ super().__init__(Path.home())
289
+
290
+ def update_config(self: Self, updates: dict[str, Any]) -> UserConfig:
291
+ """Updates multiple configuration values at once.
292
+
293
+ Args:
294
+ updates: Dictionary of key-value pairs to update
295
+
296
+ Returns:
297
+ The updated UserConfig object
298
+
299
+ Raises:
300
+ FileNotFoundError: If config file doesn't exist
301
+ ValueError: If the updates config is invalid
302
+ """
303
+ return cast("UserConfig", super().update_config(updates))
304
+
305
+
306
+ def get_fmu_directory(base_path: str | Path) -> ProjectFMUDirectory:
307
+ """Initializes access to a .fmu directory.
308
+
309
+ Args:
310
+ base_path: The directory containing the .fmu directory or one of its parent
311
+ dirs
312
+
313
+ Returns:
314
+ FMUDirectory instance
315
+
316
+ Raises:
317
+ FileExistsError: If .fmu exists but is not a directory
318
+ FileNotFoundError: If .fmu directory doesn't exist
319
+ PermissionError: If lacking permissions to read/write to the directory
320
+
321
+ """
322
+ return ProjectFMUDirectory(base_path)
323
+
324
+
325
+ def find_nearest_fmu_directory(start_path: str | Path = ".") -> ProjectFMUDirectory:
326
+ """Factory method to find and open the nearest .fmu directory.
327
+
328
+ Args:
329
+ start_path: Path to start searching from. Default current working directory
330
+
331
+ Returns:
332
+ FMUDirectory instance
333
+
334
+ Raises:
335
+ FileNotFoundError: If no .fmu directory is found
336
+ """
337
+ return ProjectFMUDirectory.find_nearest(start_path)
fmu/settings/_init.py ADDED
@@ -0,0 +1,131 @@
1
+ """Initializes the .fmu directory."""
2
+
3
+ from pathlib import Path
4
+ from textwrap import dedent
5
+ from typing import Any, Final
6
+
7
+ from ._fmu_dir import ProjectFMUDirectory, UserFMUDirectory
8
+ from ._logging import null_logger
9
+ from .models.project_config import ProjectConfig
10
+
11
+ logger: Final = null_logger(__name__)
12
+
13
+ _README = dedent("""\
14
+ This directory contains static configuration data for your FMU project.
15
+
16
+ You should *not* manually modify files within this directory. Doing so may
17
+ result in erroneous behavior or erroneous data in your FMU project.
18
+
19
+ Changes to data stored within this directory must happen through the FMU
20
+ Settings application.
21
+
22
+ Run `fmu-settings` to do this.
23
+ """)
24
+
25
+ _USER_README = dedent("""\
26
+ This directory contains static data and configuration elements used by some
27
+ components in FMU. It may also contains sensitive access tokens that should not be
28
+ shared with others.
29
+
30
+ You should *not* manually modify files within this directory. Doing so may
31
+ result in erroneous behavior by some FMU components.
32
+
33
+ Changes to data stored within this directory must happen through the FMU
34
+ Settings application.
35
+
36
+ Run `fmu-settings` to do this.
37
+ """)
38
+
39
+
40
+ def _create_fmu_directory(base_path: Path) -> None:
41
+ """Creates the .fmu directory.
42
+
43
+ Args:
44
+ base_path: Base directory where .fmu should be created
45
+
46
+ Raises:
47
+ FileNotFoundError: If base_path doesn't exist
48
+ FileExistsError: If .fmu exists
49
+ """
50
+ logger.debug(f"Creating .fmu directory in '{base_path}'")
51
+
52
+ if not base_path.exists():
53
+ raise FileNotFoundError(
54
+ f"Base path '{base_path}' does not exist. Expected the root "
55
+ "directory of an FMU project."
56
+ )
57
+
58
+ fmu_dir = base_path / ".fmu"
59
+ if fmu_dir.exists():
60
+ if fmu_dir.is_dir():
61
+ raise FileExistsError(f"{fmu_dir} already exists")
62
+ raise FileExistsError(f"{fmu_dir} exists but is not a directory")
63
+
64
+ fmu_dir.mkdir()
65
+ logger.debug(f"Created .fmu directory at '{fmu_dir}'")
66
+
67
+
68
+ def init_fmu_directory(
69
+ base_path: str | Path, config_data: ProjectConfig | dict[str, Any] | None = None
70
+ ) -> ProjectFMUDirectory:
71
+ """Creates and initializes a .fmu directory.
72
+
73
+ Also initializes a configuration file if configuration data is provided through the
74
+ function.
75
+
76
+ Args:
77
+ base_path: Directory where .fmu should be created
78
+ config_data: Optional ProjectConfig instance or dictionary with configuration
79
+ data
80
+
81
+ Returns:
82
+ Instance of FMUDirectory
83
+
84
+ Raises:
85
+ FileExistsError: If .fmu exists
86
+ FileNotFoundError: If base_path doesn't exist
87
+ PermissionError: If the user lacks permission to create directories
88
+ ValidationError: If config_data fails validationg
89
+ """
90
+ logger.debug("Initializing .fmu directory")
91
+ base_path = Path(base_path)
92
+
93
+ _create_fmu_directory(base_path)
94
+
95
+ fmu_dir = ProjectFMUDirectory(base_path)
96
+ fmu_dir.write_text_file("README", _README)
97
+
98
+ fmu_dir.config.reset()
99
+ if config_data:
100
+ if isinstance(config_data, ProjectConfig):
101
+ config_dict = config_data.model_dump()
102
+ fmu_dir.update_config(config_dict)
103
+ elif isinstance(config_data, dict):
104
+ fmu_dir.update_config(config_data)
105
+
106
+ logger.debug(f"Successfully initialized .fmu directory at '{fmu_dir}'")
107
+ return fmu_dir
108
+
109
+
110
+ def init_user_fmu_directory() -> UserFMUDirectory:
111
+ """Creates and initializes a user's $HOME/.fmu directory.
112
+
113
+ Returns:
114
+ Instance of FMUDirectory
115
+
116
+ Raises:
117
+ FileExistsError: If .fmu exists
118
+ FileNotFoundError: If base_path doesn't exist
119
+ PermissionError: If the user lacks permission to create directories
120
+ ValidationError: If config_data fails validationg
121
+ """
122
+ logger.debug("Initializing .fmu directory")
123
+
124
+ _create_fmu_directory(Path.home())
125
+
126
+ fmu_dir = UserFMUDirectory()
127
+ fmu_dir.write_text_file("README", _USER_README)
128
+
129
+ fmu_dir.config.reset()
130
+ logger.debug(f"Successfully initialized .fmu directory at '{fmu_dir}'")
131
+ return fmu_dir
@@ -0,0 +1,30 @@
1
+ """Contains the logger that should be used throughout this package."""
2
+
3
+ import logging
4
+
5
+
6
+ def null_logger(name: str) -> logging.Logger:
7
+ """Create and return a logger with a NullHandler.
8
+
9
+ This function creates a logger for the specified name and attaches a
10
+ NullHandler to it. The NullHandler prevents logging messages from being
11
+ automatically output to the console or other default handlers. This is
12
+ particularly useful in library modules where you want to provide the
13
+ users of the library the flexibility to configure their own logging behavior.
14
+
15
+ Args:
16
+ name: The name of the logger to be created. This is typically
17
+ the name of the module in which the logger is
18
+ created (e.g., using __name__).
19
+
20
+ Returns:
21
+ A logger object configured with a NullHandler.
22
+
23
+ Example:
24
+ # In a library module
25
+ logger = null_logger(__name__)
26
+ logger.info("This info won't be logged to the console by default.")
27
+ """
28
+ logger = logging.getLogger(name)
29
+ logger.addHandler(logging.NullHandler())
30
+ return logger
@@ -0,0 +1,21 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
6
+ TYPE_CHECKING = False
7
+ if TYPE_CHECKING:
8
+ from typing import Tuple
9
+ from typing import Union
10
+
11
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
12
+ else:
13
+ VERSION_TUPLE = object
14
+
15
+ version: str
16
+ __version__: str
17
+ __version_tuple__: VERSION_TUPLE
18
+ version_tuple: VERSION_TUPLE
19
+
20
+ __version__ = version = '0.0.1'
21
+ __version_tuple__ = version_tuple = (0, 0, 1)
@@ -0,0 +1,5 @@
1
+ """Contains models used in this package.
2
+
3
+ Some models contained here may also be used outside this
4
+ package.
5
+ """
@@ -0,0 +1,34 @@
1
+ """Contains enumerations used in this package."""
2
+
3
+ from enum import StrEnum
4
+
5
+
6
+ class MappingType(StrEnum):
7
+ """The discriminator used between mappings.
8
+
9
+ Each of these types should have their own mapping class derived of some sort of
10
+ mapping.
11
+ """
12
+
13
+ fault = "fault"
14
+ stratigraphy = "stratigraphy"
15
+ well = "well"
16
+
17
+
18
+ class RelationType(StrEnum):
19
+ """The kind of relation this mapping represents."""
20
+
21
+ alias = "alias"
22
+ child_to_parent = "child_to_parent"
23
+ equivalent = "equivalent"
24
+ fmu_to_target = "fmu_to_target"
25
+ predecessor_to_successor = "predecessor_to_successor"
26
+
27
+
28
+ class DataEntrySource(StrEnum):
29
+ user = "user"
30
+ automated = "automated"
31
+
32
+
33
+ class TargetSystem(StrEnum):
34
+ smda = "smda"
@@ -0,0 +1,118 @@
1
+ """Contains models used for representing mappings within FMU."""
2
+
3
+ from pathlib import Path
4
+ from typing import Literal
5
+ from uuid import UUID
6
+
7
+ from pydantic import BaseModel, RootModel
8
+
9
+ from fmu.dataio._models.enums import Content # type: ignore
10
+
11
+ from ._enums import (
12
+ DataEntrySource,
13
+ MappingType,
14
+ RelationType,
15
+ TargetSystem,
16
+ )
17
+
18
+
19
+ class Source(BaseModel):
20
+ name: str
21
+ data_entry_source: DataEntrySource
22
+
23
+
24
+ class BaseMapping(BaseModel):
25
+ """The base mapping containing the fields all mappings should contain.
26
+
27
+ These fields will be contained in every individual mapping entry.
28
+ """
29
+
30
+ source_system: str
31
+ target_system: TargetSystem
32
+ mapping_type: MappingType
33
+
34
+
35
+ class IdentifierMapping(BaseMapping):
36
+ """Base class for a one-to-one or many-to-one mapping of identifiers.
37
+
38
+ This mapping represents takes some identifier from one source and correlates it to
39
+ an identifier in a target. Most often this target will be some official masterdata
40
+ store like SMDA.
41
+ """
42
+
43
+ source_id: str
44
+ target_id: str
45
+ target_uuid: UUID
46
+
47
+
48
+ class StratigraphyMapping(IdentifierMapping):
49
+ """Represents a stratigraphy mapping.
50
+
51
+ This is a mapping from stratigraphic aliases identifiers to an official
52
+ identifier.
53
+ """
54
+
55
+ mapping_type: Literal[MappingType.stratigraphy] = MappingType.stratigraphy
56
+
57
+
58
+ class WellMapping(IdentifierMapping):
59
+ """Represents a well mapping.
60
+
61
+ This is a mapping from well aliases identifiers to an official
62
+ identifier.
63
+ """
64
+
65
+ mapping_type: Literal[MappingType.well] = MappingType.well
66
+
67
+
68
+ class FaultMapping(IdentifierMapping):
69
+ """Represents a fault mapping.
70
+
71
+ This is a mapping from fault aliases identifiers to an official
72
+ identifier.
73
+ """
74
+
75
+ mapping_type: Literal[MappingType.well]
76
+
77
+
78
+ class EntityReference(BaseModel):
79
+ """Represents one entity we wish to related to naother entity.
80
+
81
+ This is typically an object exported by dataio.
82
+ """
83
+
84
+ name: str
85
+ uuid: UUID
86
+ content: Content
87
+ relative_path: Path
88
+ absolute_path: Path
89
+
90
+
91
+ class RelationshipMapping(BaseMapping):
92
+ """Base class for a mapping that represents a relationship between two entities."""
93
+
94
+ source_entity: EntityReference
95
+ target_entity: EntityReference
96
+ relation_type: RelationType
97
+
98
+
99
+ class ParentChildMapping(BaseMapping):
100
+ """A mapping between a child and their parent."""
101
+
102
+
103
+ class HierarchicalMapping(RelationshipMapping):
104
+ """A mapping that contains a hierarchy."""
105
+
106
+
107
+ class Mappings(BaseModel):
108
+ """A list of mappings under a mappings key in metadata or in a file on disk."""
109
+
110
+ items: list[BaseMapping]
111
+
112
+
113
+ class OrderedMappings(Mappings):
114
+ """Items in this list imply an ordering that is important in some context."""
115
+
116
+
117
+ MappingFile = RootModel[Mappings]
118
+ """Represents a list of mappings contained in a text file."""