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