xtgeo 4.8.0__cp313-cp313-win_amd64.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 xtgeo might be problematic. Click here for more details.

Files changed (117) hide show
  1. cxtgeo.py +582 -0
  2. cxtgeoPYTHON_wrap.c +20938 -0
  3. xtgeo/__init__.py +246 -0
  4. xtgeo/_cxtgeo.cp313-win_amd64.pyd +0 -0
  5. xtgeo/_internal.cp313-win_amd64.pyd +0 -0
  6. xtgeo/common/__init__.py +19 -0
  7. xtgeo/common/_angles.py +29 -0
  8. xtgeo/common/_xyz_enum.py +50 -0
  9. xtgeo/common/calc.py +396 -0
  10. xtgeo/common/constants.py +30 -0
  11. xtgeo/common/exceptions.py +42 -0
  12. xtgeo/common/log.py +93 -0
  13. xtgeo/common/sys.py +166 -0
  14. xtgeo/common/types.py +18 -0
  15. xtgeo/common/version.py +21 -0
  16. xtgeo/common/xtgeo_dialog.py +604 -0
  17. xtgeo/cube/__init__.py +9 -0
  18. xtgeo/cube/_cube_export.py +214 -0
  19. xtgeo/cube/_cube_import.py +532 -0
  20. xtgeo/cube/_cube_roxapi.py +180 -0
  21. xtgeo/cube/_cube_utils.py +287 -0
  22. xtgeo/cube/_cube_window_attributes.py +340 -0
  23. xtgeo/cube/cube1.py +1023 -0
  24. xtgeo/grid3d/__init__.py +15 -0
  25. xtgeo/grid3d/_ecl_grid.py +774 -0
  26. xtgeo/grid3d/_ecl_inte_head.py +148 -0
  27. xtgeo/grid3d/_ecl_logi_head.py +71 -0
  28. xtgeo/grid3d/_ecl_output_file.py +81 -0
  29. xtgeo/grid3d/_egrid.py +1004 -0
  30. xtgeo/grid3d/_find_gridprop_in_eclrun.py +625 -0
  31. xtgeo/grid3d/_grdecl_format.py +266 -0
  32. xtgeo/grid3d/_grdecl_grid.py +388 -0
  33. xtgeo/grid3d/_grid3d.py +29 -0
  34. xtgeo/grid3d/_grid3d_fence.py +181 -0
  35. xtgeo/grid3d/_grid3d_utils.py +228 -0
  36. xtgeo/grid3d/_grid_boundary.py +76 -0
  37. xtgeo/grid3d/_grid_etc1.py +1566 -0
  38. xtgeo/grid3d/_grid_export.py +221 -0
  39. xtgeo/grid3d/_grid_hybrid.py +66 -0
  40. xtgeo/grid3d/_grid_import.py +79 -0
  41. xtgeo/grid3d/_grid_import_ecl.py +101 -0
  42. xtgeo/grid3d/_grid_import_roff.py +135 -0
  43. xtgeo/grid3d/_grid_import_xtgcpgeom.py +375 -0
  44. xtgeo/grid3d/_grid_refine.py +125 -0
  45. xtgeo/grid3d/_grid_roxapi.py +292 -0
  46. xtgeo/grid3d/_grid_wellzone.py +165 -0
  47. xtgeo/grid3d/_gridprop_export.py +178 -0
  48. xtgeo/grid3d/_gridprop_import_eclrun.py +164 -0
  49. xtgeo/grid3d/_gridprop_import_grdecl.py +130 -0
  50. xtgeo/grid3d/_gridprop_import_roff.py +52 -0
  51. xtgeo/grid3d/_gridprop_import_xtgcpprop.py +168 -0
  52. xtgeo/grid3d/_gridprop_lowlevel.py +171 -0
  53. xtgeo/grid3d/_gridprop_op1.py +174 -0
  54. xtgeo/grid3d/_gridprop_roxapi.py +239 -0
  55. xtgeo/grid3d/_gridprop_value_init.py +140 -0
  56. xtgeo/grid3d/_gridprops_import_eclrun.py +344 -0
  57. xtgeo/grid3d/_gridprops_import_roff.py +83 -0
  58. xtgeo/grid3d/_roff_grid.py +469 -0
  59. xtgeo/grid3d/_roff_parameter.py +303 -0
  60. xtgeo/grid3d/grid.py +2537 -0
  61. xtgeo/grid3d/grid_properties.py +699 -0
  62. xtgeo/grid3d/grid_property.py +1341 -0
  63. xtgeo/grid3d/types.py +15 -0
  64. xtgeo/io/__init__.py +1 -0
  65. xtgeo/io/_file.py +592 -0
  66. xtgeo/metadata/__init__.py +17 -0
  67. xtgeo/metadata/metadata.py +431 -0
  68. xtgeo/roxutils/__init__.py +7 -0
  69. xtgeo/roxutils/_roxar_loader.py +54 -0
  70. xtgeo/roxutils/_roxutils_etc.py +122 -0
  71. xtgeo/roxutils/roxutils.py +207 -0
  72. xtgeo/surface/__init__.py +18 -0
  73. xtgeo/surface/_regsurf_boundary.py +26 -0
  74. xtgeo/surface/_regsurf_cube.py +210 -0
  75. xtgeo/surface/_regsurf_cube_window.py +391 -0
  76. xtgeo/surface/_regsurf_cube_window_v2.py +297 -0
  77. xtgeo/surface/_regsurf_cube_window_v3.py +360 -0
  78. xtgeo/surface/_regsurf_export.py +388 -0
  79. xtgeo/surface/_regsurf_grid3d.py +271 -0
  80. xtgeo/surface/_regsurf_gridding.py +347 -0
  81. xtgeo/surface/_regsurf_ijxyz_parser.py +278 -0
  82. xtgeo/surface/_regsurf_import.py +347 -0
  83. xtgeo/surface/_regsurf_lowlevel.py +122 -0
  84. xtgeo/surface/_regsurf_oper.py +631 -0
  85. xtgeo/surface/_regsurf_roxapi.py +241 -0
  86. xtgeo/surface/_regsurf_utils.py +81 -0
  87. xtgeo/surface/_surfs_import.py +43 -0
  88. xtgeo/surface/_zmap_parser.py +138 -0
  89. xtgeo/surface/regular_surface.py +2967 -0
  90. xtgeo/surface/surfaces.py +276 -0
  91. xtgeo/well/__init__.py +24 -0
  92. xtgeo/well/_blockedwell_roxapi.py +221 -0
  93. xtgeo/well/_blockedwells_roxapi.py +68 -0
  94. xtgeo/well/_well_aux.py +30 -0
  95. xtgeo/well/_well_io.py +327 -0
  96. xtgeo/well/_well_oper.py +574 -0
  97. xtgeo/well/_well_roxapi.py +304 -0
  98. xtgeo/well/_wellmarkers.py +486 -0
  99. xtgeo/well/_wells_utils.py +158 -0
  100. xtgeo/well/blocked_well.py +216 -0
  101. xtgeo/well/blocked_wells.py +122 -0
  102. xtgeo/well/well1.py +1514 -0
  103. xtgeo/well/wells.py +211 -0
  104. xtgeo/xyz/__init__.py +6 -0
  105. xtgeo/xyz/_polygons_oper.py +272 -0
  106. xtgeo/xyz/_xyz.py +741 -0
  107. xtgeo/xyz/_xyz_data.py +646 -0
  108. xtgeo/xyz/_xyz_io.py +490 -0
  109. xtgeo/xyz/_xyz_lowlevel.py +42 -0
  110. xtgeo/xyz/_xyz_oper.py +613 -0
  111. xtgeo/xyz/_xyz_roxapi.py +766 -0
  112. xtgeo/xyz/points.py +681 -0
  113. xtgeo/xyz/polygons.py +811 -0
  114. xtgeo-4.8.0.dist-info/METADATA +145 -0
  115. xtgeo-4.8.0.dist-info/RECORD +117 -0
  116. xtgeo-4.8.0.dist-info/WHEEL +5 -0
  117. xtgeo-4.8.0.dist-info/licenses/LICENSE.md +165 -0
xtgeo/grid3d/types.py ADDED
@@ -0,0 +1,15 @@
1
+ """
2
+ This file contains commen types used in xtgeo, keep it free some logic.
3
+ """
4
+
5
+ from typing import Literal
6
+
7
+ METRIC = Literal[
8
+ "euclid",
9
+ "horizontal",
10
+ "east west vertical",
11
+ "north south vertical",
12
+ "x projection",
13
+ "y projection",
14
+ "z projection",
15
+ ]
xtgeo/io/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """XTGeo io module"""
xtgeo/io/_file.py ADDED
@@ -0,0 +1,592 @@
1
+ """A FileWrapper class to wrap around files and streams representing files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+ import os
7
+ import pathlib
8
+ import platform
9
+ import re
10
+ import struct
11
+ import uuid
12
+ from enum import Enum
13
+ from os.path import join
14
+ from tempfile import mkstemp
15
+ from typing import TYPE_CHECKING, Literal, Union
16
+
17
+ import xtgeo._cxtgeo
18
+ from xtgeo.common.exceptions import InvalidFileFormatError
19
+ from xtgeo.common.log import null_logger
20
+
21
+ if TYPE_CHECKING:
22
+ from xtgeo.common.types import FileLike
23
+ from xtgeo.cube import Cube
24
+ from xtgeo.grid3d import Grid, GridProperties, GridProperty
25
+ from xtgeo.surface import RegularSurface, Surfaces
26
+ from xtgeo.wells import BlockedWell, BlockedWells, Well, Wells
27
+ from xtgeo.xyz import Points, Polygons
28
+
29
+ XTGeoObject = Union[
30
+ BlockedWell,
31
+ BlockedWells,
32
+ Cube,
33
+ Grid,
34
+ GridProperty,
35
+ GridProperties,
36
+ Points,
37
+ Polygons,
38
+ RegularSurface,
39
+ Surfaces,
40
+ Well,
41
+ Wells,
42
+ ]
43
+
44
+ logger = null_logger(__name__)
45
+
46
+ VALID_FILE_ALIASES = ["$fmu-v1", "$md5sum", "$random"]
47
+
48
+
49
+ class FileFormat(Enum):
50
+ RMSWELL = ["rmswell", "rmsw", "w", "bw"]
51
+ ROFF_BINARY = ["roff_binary", "roff", "roff_bin", "roff-bin", "roffbin", "roff.*"]
52
+ ROFF_ASCII = ["roff_ascii", "roff_asc", "roff-asc", "roffasc", "asc"]
53
+ EGRID = ["egrid", "eclipserun"]
54
+ FEGRID = ["fegrid"]
55
+ INIT = ["init"]
56
+ FINIT = ["finit"]
57
+ UNRST = ["unrst"]
58
+ FUNRST = ["funrst"]
59
+ GRDECL = ["grdecl"]
60
+ BGRDECL = ["bgrdecl"]
61
+ IRAP_BINARY = [
62
+ "irap_binary",
63
+ "irap_bin",
64
+ "irapbinary",
65
+ "irap",
66
+ "rms_binary",
67
+ "irapbin",
68
+ "gri",
69
+ ]
70
+ IRAP_ASCII = [
71
+ "irapascii",
72
+ "irap_txt",
73
+ "irap_ascii",
74
+ "irap_asc",
75
+ "rms_ascii",
76
+ "irapasc",
77
+ "fgr",
78
+ ]
79
+ HDF = ["hdf", "hdf5", "h5"]
80
+ SEGY = ["segy", "sgy", "segy.*"]
81
+ STORM = ["storm", "storm_binary"]
82
+ ZMAP_ASCII = ["zmap", "zmap+", "zmap_ascii", "zmap-ascii", "zmap-asc", "zmap.*"]
83
+ IJXYZ = ["ijxyz"]
84
+ PETROMOD = ["pmd", "petromod"]
85
+ XTG = ["xtg", "xtgeo", "xtgf", "xtgcpprop", "xtg.*"]
86
+ XYZ = ["xyz", "poi", "pol"]
87
+ RMS_ATTR = ["rms_attr", "rms_attrs", "rmsattr.*"]
88
+ UNKNOWN = ["unknown"]
89
+
90
+ @staticmethod
91
+ def extensions_string(formats: list[FileFormat]) -> str:
92
+ return ", ".join([f"'{item}'" for fmt in formats for item in fmt.value])
93
+
94
+
95
+ class FileWrapper:
96
+ """
97
+ A private class for file/stream handling in/out of XTGeo and CXTGeo.
98
+
99
+ Interesting attributes:
100
+
101
+ xfile = FileWrapper(..some Path or str or BytesIO ...)
102
+
103
+ xfile.name: The absolute path to the file (str)
104
+ xfile.file: The pathlib.Path instance
105
+ xfile.memstream: Is True if memory stream
106
+
107
+ xfile.exists(): Returns True (provided mode 'r') if file exists, always True for 'w'
108
+ xfile.check_file(...): As above but may raise an Excpetion
109
+ xfile.check_folder(...): For folder; may raise an Excpetion
110
+ xfile.splitext(): return file's stem and extension
111
+ xfile.get_cfhandle(): Get C SWIG file handle
112
+ xfile.cfclose(): Close current C SWIG filehandle
113
+
114
+ """
115
+
116
+ def __init__(
117
+ self,
118
+ filelike: FileLike,
119
+ mode: Literal["r", "w", "rb", "wb"] = "rb",
120
+ obj: XTGeoObject = None,
121
+ ) -> None:
122
+ logger.debug("Init ran for FileWrapper")
123
+
124
+ if not isinstance(filelike, (str, pathlib.Path, io.BytesIO, io.StringIO)):
125
+ raise RuntimeError(
126
+ f"Cannot instantiate {self.__class__} from "
127
+ f"{filelike} of type {type(filelike)}. Expected "
128
+ f"a str, pathlib.Path, io.BytesIO, or io.StringIO."
129
+ )
130
+
131
+ if isinstance(filelike, str):
132
+ filelike = pathlib.Path(filelike)
133
+
134
+ self._file: pathlib.Path | io.BytesIO | io.StringIO = filelike
135
+ self._memstream = isinstance(self._file, (io.BytesIO, io.StringIO))
136
+ self._mode = mode
137
+ # String streams cannot be binary
138
+ if isinstance(self._file, io.StringIO) and mode in ("rb", "wb"):
139
+ self._mode = "r" if mode == "rb" else "w"
140
+
141
+ if obj and not self._memstream:
142
+ self.resolve_alias(obj)
143
+
144
+ self._cfhandle = 0
145
+ self._cfhandlecount = 0
146
+
147
+ self._tmpfile: str | None = None
148
+
149
+ logger.debug("Ran init of %s, ID is %s", __name__, id(self))
150
+
151
+ @property
152
+ def memstream(self) -> bool:
153
+ """Get whether or not this file is a io.BytesIO/StringIO memory stream."""
154
+ return self._memstream
155
+
156
+ @property
157
+ def file(self) -> pathlib.Path | io.BytesIO | io.StringIO:
158
+ """Get Path object (if input was file) or memory stream object."""
159
+ return self._file
160
+
161
+ @property
162
+ def name(self) -> str | io.BytesIO | io.StringIO:
163
+ """Get the absolute path name of the file, or the memory stream."""
164
+ if isinstance(self.file, (io.BytesIO, io.StringIO)):
165
+ return self.file
166
+
167
+ try:
168
+ logger.debug("Trying to resolve filepath")
169
+ fname = str(self.file.resolve())
170
+ except OSError:
171
+ try:
172
+ logger.debug("Trying to resolve parent, then file...")
173
+ fname = os.path.abspath(
174
+ join(str(self.file.parent.resolve()), str(self.file.name))
175
+ )
176
+ except OSError:
177
+ # means that also folder is invalid
178
+ logger.debug("Last attempt of name resolving...")
179
+ fname = os.path.abspath(str(self.file))
180
+ return fname
181
+
182
+ def resolve_alias(self, obj: XTGeoObject) -> None:
183
+ """
184
+ Change a file path name alias to autogenerated name, based on rules.
185
+
186
+ Only the file stem name will be updated, not the file name extension. Any
187
+ parent folders and file suffix/extension will be returned as is.
188
+
189
+ Aliases supported so far are '$md5sum' '$random' '$fmu-v1'
190
+
191
+ Args:
192
+ obj: Instance of some XTGeo object e.g. RegularSurface()
193
+
194
+ Example::
195
+ >>> import xtgeo
196
+ >>> surf = xtgeo.surface_from_file(surface_dir + "/topreek_rota.gri")
197
+ >>> xx = FileWrapper("/tmp/$md5sum.gri", "rb", surf)
198
+ >>> print(xx.file)
199
+ /tmp/c144fe19742adac8187b97e7976ac68c.gri
200
+
201
+ .. versionadded:: 2.14
202
+
203
+ """
204
+ if self.memstream or isinstance(self.file, (io.BytesIO, io.StringIO)):
205
+ return
206
+
207
+ parent = self.file.parent
208
+ stem = self.file.stem
209
+ suffix = self.file.suffix
210
+
211
+ if "$" in stem and stem not in VALID_FILE_ALIASES:
212
+ raise ValueError(
213
+ "A '$' is present in file name but this is not a valid alias"
214
+ )
215
+
216
+ newname = stem
217
+ if stem == "$md5sum":
218
+ newname = obj.generate_hash() # type: ignore
219
+ elif stem == "$random":
220
+ newname = uuid.uuid4().hex # random name
221
+ elif stem == "$fmu-v1":
222
+ # will make name such as topvalysar--avg_porosity based on metadata
223
+ short = obj.metadata.opt.shortname.lower().replace(" ", "_") # type: ignore
224
+ desc = obj.metadata.opt.description.lower().replace(" ", "_") # type: ignore
225
+ date = obj.metadata.opt.datetime # type: ignore
226
+ newname = short + "--" + desc
227
+ if date:
228
+ newname += "--" + date
229
+ else:
230
+ # return without modifications of self._file to avoid with_suffix() issues
231
+ # if the file name stem itself contains multiple '.'
232
+ return
233
+
234
+ self._file = (parent / newname).with_suffix(suffix)
235
+
236
+ def exists(self) -> bool:
237
+ """Returns True if 'r' file, memory stream, or folder exists."""
238
+ if "r" in self._mode:
239
+ if isinstance(self.file, (io.BytesIO, io.StringIO)):
240
+ return True
241
+ return self.file.exists()
242
+ # Writes and appends will always exist after writing
243
+ return True
244
+
245
+ def check_file(
246
+ self,
247
+ raiseerror: type[Exception] | None = None,
248
+ raisetext: str | None = None,
249
+ ) -> bool:
250
+ """
251
+ Check if a file exists, and raises an OSError if not.
252
+
253
+ This is only meaningful for 'r' files.
254
+
255
+ Args:
256
+ raiseerror: Type of exception to raise. Default is None, which means
257
+ no Exception, just return False or True.
258
+ raisetext: Which message to display if raiseerror. Defaults to None
259
+ which gives a default message.
260
+
261
+ Returns:
262
+ True if file exists and is readable, False if not.
263
+
264
+ """
265
+ logger.debug("Checking file...")
266
+
267
+ # Redundant but mypy can't follow when memstream is True
268
+ if self.memstream or isinstance(self.file, (io.BytesIO, io.StringIO)):
269
+ return True
270
+
271
+ if raisetext is None:
272
+ raisetext = f"File {self.name} does not exist or cannot be accessed"
273
+
274
+ if "r" in self._mode and (not self.file.is_file() or not self.exists()):
275
+ if raiseerror is not None:
276
+ raise raiseerror(raisetext)
277
+ return False
278
+
279
+ return True
280
+
281
+ def check_folder(
282
+ self,
283
+ raiseerror: type[Exception] | None = None,
284
+ raisetext: str | None = None,
285
+ ) -> bool:
286
+ """
287
+ Check if folder given in file exists and is writeable.
288
+
289
+ The file itself may not exist (yet), only the folder is checked.
290
+
291
+ Args:
292
+ raiseerror: Type of exception to raise. Default is None, which means
293
+ no Exception, just return False or True.
294
+ raisetext: Which message to display if raiseerror. Defaults to None
295
+ which gives a default message.
296
+
297
+ Returns:
298
+ True if folder exists and is writable, False if not.
299
+
300
+ Raises:
301
+ ValueError: If the file is a memstream
302
+
303
+ """
304
+ logger.debug("Checking folder...")
305
+
306
+ if self.memstream or isinstance(self.file, (io.BytesIO, io.StringIO)):
307
+ logger.info(
308
+ "Cannot check folder status of an in-memory file, just return True"
309
+ )
310
+ return True
311
+
312
+ folder = self.file.parent
313
+ if raisetext is None:
314
+ raisetext = f"Folder {folder.name} does not exist or cannot be accessed"
315
+
316
+ if not folder.exists():
317
+ if raiseerror:
318
+ raise raiseerror(raisetext)
319
+
320
+ return False
321
+
322
+ return True
323
+
324
+ def get_cfhandle(self) -> int:
325
+ """
326
+ Get SWIG C file handle for CXTGeo.
327
+
328
+ This is tied to cfclose() which closes the file.
329
+
330
+ if _cfhandle already exists, then _cfhandlecount is increased with 1
331
+
332
+ Returns:
333
+ int indicating the file handle number.
334
+
335
+ """
336
+ # Windows and pre-10.13 macOS lack fmemopen()
337
+ islinux = platform.system() == "Linux"
338
+
339
+ if self._cfhandle and "Swig Object of type 'FILE" in str(self._cfhandle):
340
+ self._cfhandlecount += 1
341
+ logger.debug("Get SWIG C fhandle no %s", self._cfhandlecount)
342
+ return self._cfhandle
343
+
344
+ fobj: bytes | str | io.BytesIO | io.StringIO = self.name
345
+ if isinstance(self.file, io.BytesIO):
346
+ if self._mode == "rb" and islinux:
347
+ fobj = self.file.getvalue()
348
+ elif self._mode == "wb" and islinux:
349
+ fobj = b"" # Empty bytes obj.
350
+ elif self._mode == "rb" and not islinux:
351
+ # Write stream to a temporary file
352
+ fds, self._tmpfile = mkstemp(prefix="tmpxtgeoio")
353
+ os.close(fds)
354
+ with open(self._tmpfile, "wb") as newfile:
355
+ newfile.write(self.file.getvalue())
356
+
357
+ if self.memstream:
358
+ if islinux:
359
+ cfhandle = xtgeo._cxtgeo.xtg_fopen_bytestream(fobj, self._mode)
360
+ else:
361
+ cfhandle = xtgeo._cxtgeo.xtg_fopen(self._tmpfile, self._mode)
362
+ else:
363
+ try:
364
+ cfhandle = xtgeo._cxtgeo.xtg_fopen(fobj, self._mode)
365
+ except TypeError as err:
366
+ raise OSError(f"Cannot open file: {fobj!r}") from err
367
+
368
+ self._cfhandle = cfhandle
369
+ self._cfhandlecount = 1
370
+
371
+ logger.debug("Get initial SWIG C fhandle no %s", self._cfhandlecount)
372
+ return self._cfhandle
373
+
374
+ def cfclose(self, strict: bool = True) -> bool:
375
+ """
376
+ Close SWIG C file handle by keeping track of _cfhandlecount.
377
+
378
+ Returns:
379
+ True if cfhandle is closed.
380
+
381
+ """
382
+ logger.debug("Request for closing SWIG fhandle no: %s", self._cfhandlecount)
383
+
384
+ if self._cfhandle == 0 or self._cfhandlecount == 0:
385
+ if strict:
386
+ raise RuntimeError("Ask to close a nonexisting C file handle")
387
+
388
+ self._cfhandle = 0
389
+ self._cfhandlecount = 0
390
+ return True
391
+
392
+ if self._cfhandlecount > 1:
393
+ self._cfhandlecount -= 1
394
+ logger.debug(
395
+ "Remaining SWIG cfhandles: %s, do not close...", self._cfhandlecount
396
+ )
397
+ return False
398
+
399
+ if (
400
+ isinstance(self.file, io.BytesIO)
401
+ and self._cfhandle > 0
402
+ and "w" in self._mode
403
+ ):
404
+ # this assures that the file pointer is in the end of the current filehandle
405
+ npos = xtgeo._cxtgeo.xtg_ftell(self._cfhandle)
406
+ buf = bytes(npos)
407
+
408
+ copy_code = xtgeo._cxtgeo.xtg_get_fbuffer(self._cfhandle, buf)
409
+ # Returns EXIT_SUCCESS = 0 from C
410
+ if copy_code == 0:
411
+ self.file.write(buf)
412
+ xtgeo._cxtgeo.xtg_fflush(self._cfhandle)
413
+ else:
414
+ raise RuntimeError("Could not write stream for unknown reasons")
415
+
416
+ close_code = xtgeo._cxtgeo.xtg_fclose(self._cfhandle)
417
+ if close_code != 0:
418
+ raise RuntimeError(f"Could not close C file, code {close_code}")
419
+
420
+ logger.debug("File is now closed for C io: %s", self.name)
421
+
422
+ if self._tmpfile:
423
+ try:
424
+ os.remove(self._tmpfile)
425
+ except Exception as ex:
426
+ logger.error("Could not remove tempfile for some reason: %s", ex)
427
+
428
+ self._cfhandle = 0
429
+ self._cfhandlecount = 0
430
+ logger.debug(
431
+ "Remaining SWIG cfhandles: %s, return is True", self._cfhandlecount
432
+ )
433
+ return True
434
+
435
+ def fileformat(self, fileformat: str | None = None) -> FileFormat:
436
+ """
437
+ Try to deduce format from looking at file suffix or contents.
438
+
439
+ The file signature may be the initial part of the binary file/stream but if
440
+ that fails, the file extension is used.
441
+
442
+ Args:
443
+ fileformat (str, None): An optional user-provided string indicating what
444
+ kind of file this is.
445
+
446
+ Raises:
447
+ A ValueError if an invalid or unsupported format is encountered.
448
+
449
+ Returns:
450
+ A FileFormat.
451
+ """
452
+ if fileformat:
453
+ fileformat = fileformat.lower()
454
+ self._validate_fileformat(fileformat)
455
+
456
+ fmt = self._format_from_suffix(fileformat)
457
+ if fmt == FileFormat.UNKNOWN:
458
+ fmt = self._format_from_contents()
459
+ if fmt == FileFormat.UNKNOWN:
460
+ raise InvalidFileFormatError(
461
+ f"File format {fileformat} is unknown or unsupported"
462
+ )
463
+ return fmt
464
+
465
+ def _validate_fileformat(self, fileformat: str | None) -> None:
466
+ """Validate that the pass format string is one XTGeo supports.
467
+
468
+ Raises:
469
+ ValueError: if format is unknown or unsupported
470
+ """
471
+ if not fileformat or fileformat == "guess":
472
+ return
473
+ for fmt in FileFormat:
474
+ if fileformat in fmt.value:
475
+ return
476
+ for regex in fmt.value:
477
+ if "*" in regex and re.compile(regex).match(fileformat):
478
+ return
479
+ raise InvalidFileFormatError(
480
+ f"File format {fileformat} is unknown or unsupported"
481
+ )
482
+
483
+ def _format_from_suffix(self, fileformat: str | None = None) -> FileFormat:
484
+ """Detect format by the file suffix."""
485
+ if not fileformat or fileformat == "guess":
486
+ if isinstance(self.file, (io.BytesIO, io.StringIO)):
487
+ return FileFormat.UNKNOWN
488
+ fileformat = self.file.suffix[1:].lower()
489
+
490
+ for fmt in FileFormat:
491
+ if fileformat in fmt.value:
492
+ logger.debug("Extension hints: %s", fmt)
493
+ return fmt
494
+
495
+ # Fall back to regex
496
+ for fmt in FileFormat:
497
+ for regex in fmt.value:
498
+ if "*" in regex and re.compile(regex).match(fileformat):
499
+ logger.debug("Extension by regexp hints %s", fmt)
500
+ return fmt
501
+
502
+ return FileFormat.UNKNOWN
503
+
504
+ def _format_from_contents(self) -> FileFormat:
505
+ BUFFER_SIZE = 128
506
+ buffer = bytearray(BUFFER_SIZE)
507
+
508
+ if isinstance(self.file, (io.BytesIO, io.StringIO)):
509
+ mark = self.file.tell()
510
+ # Encode to bytes if string
511
+ if isinstance(self.file, io.StringIO):
512
+ strbuf = self.file.read(BUFFER_SIZE)
513
+ buffer = bytearray(strbuf.encode())
514
+ else:
515
+ self.file.readinto(buffer)
516
+ self.file.seek(mark)
517
+ else:
518
+ if not self.exists():
519
+ raise FileNotFoundError(f"File {self.name} does not exist")
520
+ with open(self.file, "rb") as fhandle:
521
+ fhandle.readinto(buffer)
522
+
523
+ # HDF format, different variants
524
+ if len(buffer) >= 4:
525
+ _, hdf = struct.unpack("b 3s", buffer[:4])
526
+ if hdf == b"HDF":
527
+ logger.debug("Signature is hdf")
528
+ return FileFormat.HDF
529
+
530
+ # Irap binary regular surface format
531
+ if len(buffer) >= 8:
532
+ fortranblock, gricode = struct.unpack(">ii", buffer[:8])
533
+ if fortranblock == 32 and gricode == -996:
534
+ logger.debug("Signature is irap binary")
535
+ return FileFormat.IRAP_BINARY
536
+
537
+ # Petromod binary regular surface
538
+ if b"Content=Map" in buffer and b"DataUnitDistance" in buffer:
539
+ logger.debug("Signature is petromod")
540
+ return FileFormat.PETROMOD
541
+
542
+ # Eclipse binary 3D EGRID, look at FILEHEAD:
543
+ # 'FILEHEAD' 100 'INTE'
544
+ # 3 2016 0 0 0 0
545
+ # (ver) (release) (reserved) (backw) (gtype) (dualporo)
546
+ if len(buffer) >= 24:
547
+ fort1, name, num, _, fort2 = struct.unpack("> i 8s i 4s i", buffer[:24])
548
+ if fort1 == 16 and name == b"FILEHEAD" and num == 100 and fort2 == 16:
549
+ logger.debug("Signature is egrid")
550
+ return FileFormat.EGRID
551
+ # Eclipse binary 3D UNRST, look for SEQNUM:
552
+ # 'SEQNUM' 1 'INTE'
553
+ if fort1 == 16 and name == b"SEQNUM " and num == 1 and fort2 == 16:
554
+ logger.debug("Signature is unrst")
555
+ return FileFormat.UNRST
556
+ # Eclipse binary 3D INIT, look for INTEHEAD:
557
+ # 'INTEHEAD' 411 'INTE'
558
+ if fort1 == 16 and name == b"INTEHEAD" and num > 400 and fort2 == 16:
559
+ logger.debug("Signature is init")
560
+ return FileFormat.INIT
561
+
562
+ if len(buffer) >= 9:
563
+ name, _ = struct.unpack("8s b", buffer[:9])
564
+ if name == b"roff-bin":
565
+ logger.debug("Signature is roff_binary")
566
+ return FileFormat.ROFF_BINARY
567
+ if name == b"roff-asc":
568
+ logger.debug("Signature is roff_ascii")
569
+ return FileFormat.ROFF_ASCII
570
+
571
+ # RMS well format (ascii)
572
+ # 1.0
573
+ # Unknown
574
+ # WELL12 90941.63200000004 5506367.711 23.0
575
+ # ...
576
+ # The signature here is one float in first line with values 1.0; one string
577
+ # in second line; and 3 or 4 items in the next (sometimes RKB is missing)
578
+ try:
579
+ xbuf = buffer.decode().split("\n")
580
+ except UnicodeDecodeError:
581
+ return FileFormat.UNKNOWN
582
+
583
+ if (
584
+ len(xbuf) >= 3
585
+ and xbuf[0] == "1.0"
586
+ and len(xbuf[1]) >= 1
587
+ and len(xbuf[2]) >= 10
588
+ ):
589
+ logger.debug("Signature is rmswell")
590
+ return FileFormat.RMSWELL
591
+
592
+ return FileFormat.UNKNOWN
@@ -0,0 +1,17 @@
1
+ """XTGeo metadata package."""
2
+
3
+ from .metadata import (
4
+ MetaDataCPGeometry,
5
+ MetaDataCPProperty,
6
+ MetaDataRegularCube,
7
+ MetaDataRegularSurface,
8
+ MetaDataWell,
9
+ )
10
+
11
+ __all__ = [
12
+ "MetaDataRegularCube",
13
+ "MetaDataRegularSurface",
14
+ "MetaDataCPGeometry",
15
+ "MetaDataCPProperty",
16
+ "MetaDataWell",
17
+ ]