foamlib 0.3.11__py3-none-any.whl → 0.3.12__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.
foamlib/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """A Python interface for interacting with OpenFOAM."""
2
2
 
3
- __version__ = "0.3.11"
3
+ __version__ = "0.3.12"
4
4
 
5
5
  from ._cases import AsyncFoamCase, FoamCase, FoamCaseBase
6
6
  from ._files import FoamDict, FoamFieldFile, FoamFile
@@ -0,0 +1,9 @@
1
+ from ._async import AsyncFoamCase
2
+ from ._base import FoamCaseBase
3
+ from ._sync import FoamCase
4
+
5
+ __all__ = [
6
+ "FoamCaseBase",
7
+ "FoamCase",
8
+ "AsyncFoamCase",
9
+ ]
@@ -0,0 +1,157 @@
1
+ import asyncio
2
+ import multiprocessing
3
+ import sys
4
+ from contextlib import asynccontextmanager
5
+ from pathlib import Path
6
+ from typing import (
7
+ Optional,
8
+ Union,
9
+ )
10
+
11
+ if sys.version_info >= (3, 9):
12
+ from collections.abc import AsyncGenerator, Sequence
13
+ else:
14
+ from typing import AsyncGenerator, Sequence
15
+
16
+ import aioshutil
17
+
18
+ from .._util import run_process_async
19
+ from ._base import FoamCaseBase
20
+
21
+
22
+ class AsyncFoamCase(FoamCaseBase):
23
+ """
24
+ An OpenFOAM case with asynchronous support.
25
+
26
+ Provides methods for running and cleaning cases, as well as accessing files.
27
+
28
+ Access the time directories of the case as a sequence, e.g. `case[0]` or `case[-1]`.
29
+
30
+ :param path: The path to the case directory.
31
+ """
32
+
33
+ max_cpus = multiprocessing.cpu_count()
34
+ """
35
+ Maximum number of CPUs to use for running `AsyncFoamCase`s concurrently. Defaults to the number of CPUs on the system.
36
+ """
37
+
38
+ _reserved_cpus = 0
39
+ _cpus_cond = None # Cannot be initialized here yet
40
+
41
+ @staticmethod
42
+ @asynccontextmanager
43
+ async def _cpus(cpus: int) -> AsyncGenerator[None, None]:
44
+ if AsyncFoamCase._cpus_cond is None:
45
+ AsyncFoamCase._cpus_cond = asyncio.Condition()
46
+
47
+ cpus = min(cpus, AsyncFoamCase.max_cpus)
48
+ if cpus > 0:
49
+ async with AsyncFoamCase._cpus_cond:
50
+ await AsyncFoamCase._cpus_cond.wait_for(
51
+ lambda: AsyncFoamCase.max_cpus - AsyncFoamCase._reserved_cpus
52
+ >= cpus
53
+ )
54
+ AsyncFoamCase._reserved_cpus += cpus
55
+ try:
56
+ yield
57
+ finally:
58
+ if cpus > 0:
59
+ async with AsyncFoamCase._cpus_cond:
60
+ AsyncFoamCase._reserved_cpus -= cpus
61
+ AsyncFoamCase._cpus_cond.notify(cpus)
62
+
63
+ async def clean(
64
+ self,
65
+ *,
66
+ script: bool = True,
67
+ check: bool = False,
68
+ ) -> None:
69
+ """
70
+ Clean this case.
71
+
72
+ :param script: If True, use an (All)clean script if it exists. If False, ignore any clean scripts.
73
+ :param check: If True, raise a CalledProcessError if the clean script returns a non-zero exit code.
74
+ """
75
+ script_path = self._clean_script() if script else None
76
+
77
+ if script_path is not None:
78
+ await self.run([script_path], check=check)
79
+ else:
80
+ for p in self._clean_paths():
81
+ if p.is_dir():
82
+ await aioshutil.rmtree(p) # type: ignore [call-arg]
83
+ else:
84
+ p.unlink()
85
+
86
+ async def _run(
87
+ self,
88
+ cmd: Union[Sequence[Union[str, Path]], str, Path],
89
+ *,
90
+ check: bool = True,
91
+ ) -> None:
92
+ await run_process_async(cmd, cwd=self.path, check=check)
93
+
94
+ async def run(
95
+ self,
96
+ cmd: Optional[Union[Sequence[Union[str, Path]], str, Path]] = None,
97
+ *,
98
+ script: bool = True,
99
+ parallel: Optional[bool] = None,
100
+ cpus: Optional[int] = None,
101
+ check: bool = True,
102
+ ) -> None:
103
+ for name, args, kwargs in self._run_cmds(
104
+ cmd=cmd, script=script, parallel=parallel, check=check
105
+ ):
106
+ if cpus is None:
107
+ cpus = 1
108
+
109
+ if name == "run" and kwargs.get("parallel", False):
110
+ cpus = min(self._nprocessors, cpus)
111
+
112
+ async with self._cpus(cpus):
113
+ await getattr(self, name)(*args, **kwargs)
114
+
115
+ async def block_mesh(self, *, check: bool = True) -> None:
116
+ """Run blockMesh on this case."""
117
+ await self.run(["blockMesh"], check=check)
118
+
119
+ async def decompose_par(self, *, check: bool = True) -> None:
120
+ """Decompose this case for parallel running."""
121
+ await self.run(["decomposePar"], check=check)
122
+
123
+ async def reconstruct_par(self, *, check: bool = True) -> None:
124
+ """Reconstruct this case after parallel running."""
125
+ await self.run(["reconstructPar"], check=check)
126
+
127
+ async def restore_0_dir(self) -> None:
128
+ """Restore the 0 directory from the 0.orig directory."""
129
+ await aioshutil.rmtree(self.path / "0", ignore_errors=True) # type: ignore [call-arg]
130
+ await aioshutil.copytree(self.path / "0.orig", self.path / "0")
131
+
132
+ async def copy(self, dest: Union[Path, str]) -> "AsyncFoamCase":
133
+ """
134
+ Make a copy of this case.
135
+
136
+ :param dest: The destination path.
137
+ """
138
+ return AsyncFoamCase(await aioshutil.copytree(self.path, dest, symlinks=True))
139
+
140
+ async def clone(self, dest: Union[Path, str]) -> "AsyncFoamCase":
141
+ """
142
+ Clone this case (make a clean copy).
143
+
144
+ :param dest: The destination path.
145
+ """
146
+ if self._clean_script() is not None:
147
+ copy = await self.copy(dest)
148
+ await copy.clean()
149
+ return copy
150
+
151
+ dest = Path(dest)
152
+
153
+ await aioshutil.copytree(
154
+ self.path, dest, symlinks=True, ignore=self._clone_ignore()
155
+ )
156
+
157
+ return AsyncFoamCase(dest)
@@ -0,0 +1,354 @@
1
+ import sys
2
+ from pathlib import Path
3
+ from typing import (
4
+ Any,
5
+ Optional,
6
+ Tuple,
7
+ Union,
8
+ overload,
9
+ )
10
+
11
+ if sys.version_info >= (3, 9):
12
+ from collections.abc import (
13
+ Callable,
14
+ Collection,
15
+ Generator,
16
+ Iterator,
17
+ Mapping,
18
+ Sequence,
19
+ Set,
20
+ )
21
+ else:
22
+ from typing import AbstractSet as Set
23
+ from typing import (
24
+ Callable,
25
+ Collection,
26
+ Generator,
27
+ Iterator,
28
+ Mapping,
29
+ Sequence,
30
+ )
31
+
32
+
33
+ from .._files import FoamFieldFile, FoamFile
34
+ from .._util import is_sequence
35
+
36
+
37
+ class FoamCaseBase(Sequence["FoamCaseBase.TimeDirectory"]):
38
+ def __init__(self, path: Union[Path, str] = Path()):
39
+ self.path = Path(path).absolute()
40
+
41
+ class TimeDirectory(Set[FoamFieldFile]):
42
+ """
43
+ An OpenFOAM time directory in a case.
44
+
45
+ Use to access field files in the directory, e.g. `time["U"]`.
46
+
47
+ :param path: The path to the time directory.
48
+ """
49
+
50
+ def __init__(self, path: Union[Path, str]):
51
+ self.path = Path(path).absolute()
52
+
53
+ @property
54
+ def time(self) -> float:
55
+ """The time that corresponds to this directory."""
56
+ return float(self.path.name)
57
+
58
+ @property
59
+ def name(self) -> str:
60
+ """The name of this time directory."""
61
+ return self.path.name
62
+
63
+ def __getitem__(self, key: str) -> FoamFieldFile:
64
+ if (self.path / key).is_file():
65
+ return FoamFieldFile(self.path / key)
66
+ elif (self.path / f"{key}.gz").is_file():
67
+ return FoamFieldFile(self.path / f"{key}.gz")
68
+ else:
69
+ raise KeyError(key)
70
+
71
+ def __contains__(self, obj: object) -> bool:
72
+ if isinstance(obj, FoamFieldFile):
73
+ return obj.path.parent == self.path
74
+ elif isinstance(obj, str):
75
+ return (self.path / obj).is_file() or (
76
+ self.path / f"{obj}.gz"
77
+ ).is_file()
78
+ else:
79
+ return False
80
+
81
+ def __iter__(self) -> Iterator[FoamFieldFile]:
82
+ for p in self.path.iterdir():
83
+ if p.is_file() and (
84
+ p.suffix != ".gz" or not p.with_suffix("").is_file()
85
+ ):
86
+ yield FoamFieldFile(p)
87
+
88
+ def __len__(self) -> int:
89
+ return len(list(iter(self)))
90
+
91
+ def __fspath__(self) -> str:
92
+ return str(self.path)
93
+
94
+ def __repr__(self) -> str:
95
+ return f"{type(self).__qualname__}('{self.path}')"
96
+
97
+ def __str__(self) -> str:
98
+ return str(self.path)
99
+
100
+ @property
101
+ def _times(self) -> Sequence["FoamCaseBase.TimeDirectory"]:
102
+ times = []
103
+ for p in self.path.iterdir():
104
+ if p.is_dir():
105
+ try:
106
+ float(p.name)
107
+ except ValueError:
108
+ pass
109
+ else:
110
+ times.append(FoamCaseBase.TimeDirectory(p))
111
+
112
+ times.sort(key=lambda t: t.time)
113
+
114
+ return times
115
+
116
+ @overload
117
+ def __getitem__(
118
+ self, index: Union[int, float, str]
119
+ ) -> "FoamCaseBase.TimeDirectory": ...
120
+
121
+ @overload
122
+ def __getitem__(self, index: slice) -> Sequence["FoamCaseBase.TimeDirectory"]: ...
123
+
124
+ def __getitem__(
125
+ self, index: Union[int, slice, float, str]
126
+ ) -> Union["FoamCaseBase.TimeDirectory", Sequence["FoamCaseBase.TimeDirectory"]]:
127
+ if isinstance(index, str):
128
+ return FoamCaseBase.TimeDirectory(self.path / index)
129
+ elif isinstance(index, float):
130
+ for time in self._times:
131
+ if time.time == index:
132
+ return time
133
+ raise IndexError(f"Time {index} not found")
134
+ return self._times[index]
135
+
136
+ def __len__(self) -> int:
137
+ return len(self._times)
138
+
139
+ def _clean_paths(self) -> Set[Path]:
140
+ has_decompose_par_dict = (self.path / "system" / "decomposeParDict").is_file()
141
+ has_block_mesh_dict = (self.path / "system" / "blockMeshDict").is_file()
142
+
143
+ paths = set()
144
+
145
+ for p in self.path.iterdir():
146
+ if p.is_dir():
147
+ try:
148
+ t = float(p.name)
149
+ except ValueError:
150
+ pass
151
+ else:
152
+ if t != 0:
153
+ paths.add(p)
154
+
155
+ if has_decompose_par_dict and p.name.startswith("processor"):
156
+ paths.add(p)
157
+
158
+ if (self.path / "0.orig").is_dir() and (self.path / "0").is_dir():
159
+ paths.add(self.path / "0")
160
+
161
+ if has_block_mesh_dict and (self.path / "constant" / "polyMesh").exists():
162
+ paths.add(self.path / "constant" / "polyMesh")
163
+
164
+ if self._run_script() is not None:
165
+ paths.update(self.path.glob("log.*"))
166
+
167
+ return paths
168
+
169
+ def _clone_ignore(
170
+ self,
171
+ ) -> Callable[[Union[Path, str], Collection[str]], Collection[str]]:
172
+ clean_paths = self._clean_paths()
173
+
174
+ def ignore(path: Union[Path, str], names: Collection[str]) -> Collection[str]:
175
+ paths = {Path(path) / name for name in names}
176
+ return {p.name for p in paths.intersection(clean_paths)}
177
+
178
+ return ignore
179
+
180
+ def _clean_script(self) -> Optional[Path]:
181
+ """Return the path to the (All)clean script, or None if no clean script is found."""
182
+ clean = self.path / "clean"
183
+ all_clean = self.path / "Allclean"
184
+
185
+ if clean.is_file():
186
+ return clean
187
+ elif all_clean.is_file():
188
+ return all_clean
189
+ else:
190
+ return None
191
+
192
+ def _run_script(self, *, parallel: Optional[bool] = None) -> Optional[Path]:
193
+ """Return the path to the (All)run script, or None if no run script is found."""
194
+ run = self.path / "run"
195
+ run_parallel = self.path / "run-parallel"
196
+ all_run = self.path / "Allrun"
197
+ all_run_parallel = self.path / "Allrun-parallel"
198
+
199
+ if run.is_file() or all_run.is_file():
200
+ if run_parallel.is_file() or all_run_parallel.is_file():
201
+ if parallel:
202
+ return run_parallel if run_parallel.is_file() else all_run_parallel
203
+ elif parallel is False:
204
+ return run if run.is_file() else all_run
205
+ else:
206
+ raise ValueError(
207
+ "Both (All)run and (All)run-parallel scripts are present. Please specify parallel argument."
208
+ )
209
+ return run if run.is_file() else all_run
210
+ elif parallel is not False and (
211
+ run_parallel.is_file() or all_run_parallel.is_file()
212
+ ):
213
+ return run_parallel if run_parallel.is_file() else all_run_parallel
214
+ else:
215
+ return None
216
+
217
+ def _run_cmds(
218
+ self,
219
+ cmd: Optional[Union[Sequence[Union[str, Path]], str, Path]] = None,
220
+ *,
221
+ script: bool = True,
222
+ parallel: Optional[bool] = None,
223
+ check: bool = True,
224
+ ) -> Generator[Tuple[str, Sequence[Any], Mapping[str, Any]], None, None]:
225
+ if cmd is not None:
226
+ if parallel:
227
+ cmd = self._parallel_cmd(cmd)
228
+
229
+ yield ("_run", (cmd,), {"check": check})
230
+ else:
231
+ script_path = self._run_script(parallel=parallel) if script else None
232
+
233
+ if script_path is not None:
234
+ yield ("_run", ([script_path],), {"check": check})
235
+
236
+ else:
237
+ if not self and (self.path / "0.orig").is_dir():
238
+ yield ("restore_0_dir", (), {})
239
+
240
+ if (self.path / "system" / "blockMeshDict").is_file():
241
+ yield ("block_mesh", (), {"check": check})
242
+
243
+ if parallel is None:
244
+ parallel = (
245
+ self._nprocessors > 0
246
+ or (self.path / "system" / "decomposeParDict").is_file()
247
+ )
248
+
249
+ if parallel:
250
+ if (
251
+ self._nprocessors == 0
252
+ and (self.path / "system" / "decomposeParDict").is_file()
253
+ ):
254
+ yield ("decompose_par", (), {"check": check})
255
+
256
+ yield (
257
+ "run",
258
+ ([self.application],),
259
+ {"parallel": parallel, "check": check},
260
+ )
261
+
262
+ def _parallel_cmd(
263
+ self, cmd: Union[Sequence[Union[str, Path]], str, Path]
264
+ ) -> Union[Sequence[Union[str, Path]], str]:
265
+ if not is_sequence(cmd):
266
+ return f"mpiexec -np {self._nprocessors} {cmd} -parallel"
267
+ else:
268
+ return [
269
+ "mpiexec",
270
+ "-np",
271
+ str(self._nprocessors),
272
+ cmd[0],
273
+ "-parallel",
274
+ *cmd[1:],
275
+ ]
276
+
277
+ @property
278
+ def name(self) -> str:
279
+ """The name of the case."""
280
+ return self.path.name
281
+
282
+ def file(self, path: Union[Path, str]) -> FoamFile:
283
+ """Return a FoamFile object for the given path in the case."""
284
+ return FoamFile(self.path / path)
285
+
286
+ @property
287
+ def _nsubdomains(self) -> Optional[int]:
288
+ """Return the number of subdomains as set in the decomposeParDict, or None if no decomposeParDict is found."""
289
+ try:
290
+ nsubdomains = self.decompose_par_dict["numberOfSubdomains"]
291
+ if not isinstance(nsubdomains, int):
292
+ raise TypeError(
293
+ f"numberOfSubdomains in {self.decompose_par_dict} is not an integer"
294
+ )
295
+ return nsubdomains
296
+ except FileNotFoundError:
297
+ return None
298
+
299
+ @property
300
+ def _nprocessors(self) -> int:
301
+ """Return the number of processor directories in the case."""
302
+ return len(list(self.path.glob("processor*")))
303
+
304
+ @property
305
+ def application(self) -> str:
306
+ """The application name as set in the controlDict."""
307
+ application = self.control_dict["application"]
308
+ if not isinstance(application, str):
309
+ raise TypeError(f"application in {self.control_dict} is not a string")
310
+ return application
311
+
312
+ @property
313
+ def control_dict(self) -> FoamFile:
314
+ """The controlDict file."""
315
+ return self.file("system/controlDict")
316
+
317
+ @property
318
+ def fv_schemes(self) -> FoamFile:
319
+ """The fvSchemes file."""
320
+ return self.file("system/fvSchemes")
321
+
322
+ @property
323
+ def fv_solution(self) -> FoamFile:
324
+ """The fvSolution file."""
325
+ return self.file("system/fvSolution")
326
+
327
+ @property
328
+ def decompose_par_dict(self) -> FoamFile:
329
+ """The decomposeParDict file."""
330
+ return self.file("system/decomposeParDict")
331
+
332
+ @property
333
+ def block_mesh_dict(self) -> FoamFile:
334
+ """The blockMeshDict file."""
335
+ return self.file("system/blockMeshDict")
336
+
337
+ @property
338
+ def transport_properties(self) -> FoamFile:
339
+ """The transportProperties file."""
340
+ return self.file("constant/transportProperties")
341
+
342
+ @property
343
+ def turbulence_properties(self) -> FoamFile:
344
+ """The turbulenceProperties file."""
345
+ return self.file("constant/turbulenceProperties")
346
+
347
+ def __fspath__(self) -> str:
348
+ return str(self.path)
349
+
350
+ def __repr__(self) -> str:
351
+ return f"{type(self).__qualname__}('{self.path}')"
352
+
353
+ def __str__(self) -> str:
354
+ return str(self.path)
@@ -0,0 +1,121 @@
1
+ import shutil
2
+ import sys
3
+ from pathlib import Path
4
+ from typing import (
5
+ Optional,
6
+ Union,
7
+ )
8
+
9
+ if sys.version_info >= (3, 9):
10
+ from collections.abc import Sequence
11
+ else:
12
+ from typing import Sequence
13
+
14
+ from .._util import run_process
15
+ from ._base import FoamCaseBase
16
+
17
+
18
+ class FoamCase(FoamCaseBase):
19
+ """
20
+ An OpenFOAM case.
21
+
22
+ Provides methods for running and cleaning cases, as well as accessing files.
23
+
24
+ Access the time directories of the case as a sequence, e.g. `case[0]` or `case[-1]`.
25
+
26
+ :param path: The path to the case directory.
27
+ """
28
+
29
+ def clean(
30
+ self,
31
+ *,
32
+ script: bool = True,
33
+ check: bool = False,
34
+ ) -> None:
35
+ """
36
+ Clean this case.
37
+
38
+ :param script: If True, use an (All)clean script if it exists. If False, ignore any clean scripts.
39
+ :param check: If True, raise a CalledProcessError if the clean script returns a non-zero exit code.
40
+ """
41
+ script_path = self._clean_script() if script else None
42
+
43
+ if script_path is not None:
44
+ self.run([script_path], check=check)
45
+ else:
46
+ for p in self._clean_paths():
47
+ if p.is_dir():
48
+ shutil.rmtree(p)
49
+ else:
50
+ p.unlink()
51
+
52
+ def _run(
53
+ self,
54
+ cmd: Union[Sequence[Union[str, Path]], str, Path],
55
+ *,
56
+ check: bool = True,
57
+ ) -> None:
58
+ run_process(cmd, cwd=self.path, check=check)
59
+
60
+ def run(
61
+ self,
62
+ cmd: Optional[Union[Sequence[Union[str, Path]], str, Path]] = None,
63
+ *,
64
+ script: bool = True,
65
+ parallel: Optional[bool] = None,
66
+ check: bool = True,
67
+ ) -> None:
68
+ """
69
+ Run this case, or a specified command in the context of this case.
70
+
71
+ :param cmd: The command to run. If None, run the case. If a sequence, the first element is the command and the rest are arguments. If a string, `cmd` is executed in a shell.
72
+ :param script: If True and `cmd` is None, use an (All)run(-parallel) script if it exists for running the case. If False or no run script is found, autodetermine the command(s) needed to run the case.
73
+ :param parallel: If True, run in parallel using MPI. If None, autodetect whether to run in parallel.
74
+ :param check: If True, raise a CalledProcessError if any command returns a non-zero exit code.
75
+ """
76
+ for name, args, kwargs in self._run_cmds(
77
+ cmd=cmd, script=script, parallel=parallel, check=check
78
+ ):
79
+ getattr(self, name)(*args, **kwargs)
80
+
81
+ def block_mesh(self, *, check: bool = True) -> None:
82
+ """Run blockMesh on this case."""
83
+ self.run(["blockMesh"], check=check)
84
+
85
+ def decompose_par(self, *, check: bool = True) -> None:
86
+ """Decompose this case for parallel running."""
87
+ self.run(["decomposePar"], check=check)
88
+
89
+ def reconstruct_par(self, *, check: bool = True) -> None:
90
+ """Reconstruct this case after parallel running."""
91
+ self.run(["reconstructPar"], check=check)
92
+
93
+ def restore_0_dir(self) -> None:
94
+ """Restore the 0 directory from the 0.orig directory."""
95
+ shutil.rmtree(self.path / "0", ignore_errors=True)
96
+ shutil.copytree(self.path / "0.orig", self.path / "0")
97
+
98
+ def copy(self, dest: Union[Path, str]) -> "FoamCase":
99
+ """
100
+ Make a copy of this case.
101
+
102
+ :param dest: The destination path.
103
+ """
104
+ return FoamCase(shutil.copytree(self.path, dest, symlinks=True))
105
+
106
+ def clone(self, dest: Union[Path, str]) -> "FoamCase":
107
+ """
108
+ Clone this case (make a clean copy).
109
+
110
+ :param dest: The destination path.
111
+ """
112
+ if self._clean_script() is not None:
113
+ copy = self.copy(dest)
114
+ copy.clean()
115
+ return copy
116
+
117
+ dest = Path(dest)
118
+
119
+ shutil.copytree(self.path, dest, symlinks=True, ignore=self._clone_ignore())
120
+
121
+ return FoamCase(dest)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: foamlib
3
- Version: 0.3.11
3
+ Version: 0.3.12
4
4
  Summary: A Python interface for interacting with OpenFOAM
5
5
  Author-email: "Gabriel S. Gerlero" <ggerlero@cimec.unl.edu.ar>
6
6
  Project-URL: Homepage, https://github.com/gerlero/foamlib
@@ -36,7 +36,7 @@ Requires-Dist: foamlib[typing] ; extra == 'dev'
36
36
  Requires-Dist: foamlib[docs] ; extra == 'dev'
37
37
  Provides-Extra: docs
38
38
  Requires-Dist: foamlib[numpy] ; extra == 'docs'
39
- Requires-Dist: sphinx <8,>=7 ; extra == 'docs'
39
+ Requires-Dist: sphinx <9,>=7 ; extra == 'docs'
40
40
  Requires-Dist: sphinx-rtd-theme ; extra == 'docs'
41
41
  Provides-Extra: lint
42
42
  Requires-Dist: ruff ; extra == 'lint'
@@ -0,0 +1,18 @@
1
+ foamlib/__init__.py,sha256=vhhhADNxwAVfkJ0_bm28V9p2b-Mo5Mgua44ZIo2hl88,432
2
+ foamlib/_util.py,sha256=vL03aAzpWdZyYIhe2WTxHiz9b4lnttVnRuzqUmZvXXk,3047
3
+ foamlib/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ foamlib/_cases/__init__.py,sha256=4bHQRLtYh0IBOuuCSwUbipZEPqIF0sypU2xtoc6YW3c,166
5
+ foamlib/_cases/_async.py,sha256=Rfbq3rerYZdt4lX7YxnStj4LSqwH2j0rAmbPkSDHhP0,4995
6
+ foamlib/_cases/_base.py,sha256=jHgZAR1z7pZHPC0XTDTNXVfTJ8BKFv_ESYwUz2NPzWw,11416
7
+ foamlib/_cases/_sync.py,sha256=irMprDmcvjvdIj82griGhXvDU_L9UnyAbyJjS74Z3ug,3961
8
+ foamlib/_files/__init__.py,sha256=vDkPj8u8bX_I_m2YfeKvXBgwg8D1ufyFCfHGHKN3JPQ,140
9
+ foamlib/_files/_base.py,sha256=YA5a-i5HZuA3JslCD6r-DwZzpSA8r42dqSXef286Ako,2050
10
+ foamlib/_files/_files.py,sha256=4rZb3HMJADmJBGmRpChiujJNX1UIxhgHI5YPEmEOWvE,10784
11
+ foamlib/_files/_io.py,sha256=pGYMoLI5Dz2Y90EB1CtklZiffpDTGuQbbbkyqRe0JAo,2115
12
+ foamlib/_files/_parsing.py,sha256=SW1c1adDTg3e65hhZv5ZFDX8j654E88FDn13zl8gJ8c,7694
13
+ foamlib/_files/_serialization.py,sha256=LCeaLWtNvkcs0dfowL7nViiByxw7U_fvgueVjFliipU,3462
14
+ foamlib-0.3.12.dist-info/LICENSE.txt,sha256=5Dte9TUnLZzPRs4NQzl-Jc2-Ljd-t_v0ZR5Ng5r0UsY,35131
15
+ foamlib-0.3.12.dist-info/METADATA,sha256=5DDxoDLrNBs1fXupfQCTxSzb6Z1a3w34IvImEgiSrRc,5458
16
+ foamlib-0.3.12.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
17
+ foamlib-0.3.12.dist-info/top_level.txt,sha256=ZdVYtetXGwPwyfL-WhlhbTFQGAwKX5P_gXxtH9JYFPI,8
18
+ foamlib-0.3.12.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (71.1.0)
2
+ Generator: setuptools (72.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
foamlib/_cases.py DELETED
@@ -1,630 +0,0 @@
1
- import asyncio
2
- import multiprocessing
3
- import shutil
4
- import sys
5
- from contextlib import asynccontextmanager
6
- from pathlib import Path
7
- from typing import (
8
- Optional,
9
- Union,
10
- overload,
11
- )
12
-
13
- if sys.version_info >= (3, 9):
14
- from collections.abc import (
15
- AsyncGenerator,
16
- Callable,
17
- Collection,
18
- Iterator,
19
- Sequence,
20
- Set,
21
- )
22
- else:
23
- from typing import AbstractSet as Set
24
- from typing import AsyncGenerator, Callable, Collection, Iterator, Sequence
25
-
26
- import aioshutil
27
-
28
- from ._files import FoamFieldFile, FoamFile
29
- from ._util import is_sequence, run_process, run_process_async
30
-
31
-
32
- class FoamCaseBase(Sequence["FoamCaseBase.TimeDirectory"]):
33
- def __init__(self, path: Union[Path, str] = Path()):
34
- self.path = Path(path).absolute()
35
-
36
- class TimeDirectory(Set[FoamFieldFile]):
37
- """
38
- An OpenFOAM time directory in a case.
39
-
40
- Use to access field files in the directory, e.g. `time["U"]`.
41
-
42
- :param path: The path to the time directory.
43
- """
44
-
45
- def __init__(self, path: Union[Path, str]):
46
- self.path = Path(path).absolute()
47
-
48
- @property
49
- def time(self) -> float:
50
- """The time that corresponds to this directory."""
51
- return float(self.path.name)
52
-
53
- @property
54
- def name(self) -> str:
55
- """The name of this time directory."""
56
- return self.path.name
57
-
58
- def __getitem__(self, key: str) -> FoamFieldFile:
59
- if (self.path / key).is_file():
60
- return FoamFieldFile(self.path / key)
61
- elif (self.path / f"{key}.gz").is_file():
62
- return FoamFieldFile(self.path / f"{key}.gz")
63
- else:
64
- raise KeyError(key)
65
-
66
- def __contains__(self, obj: object) -> bool:
67
- if isinstance(obj, FoamFieldFile):
68
- return obj.path.parent == self.path
69
- elif isinstance(obj, str):
70
- return (self.path / obj).is_file() or (
71
- self.path / f"{obj}.gz"
72
- ).is_file()
73
- else:
74
- return False
75
-
76
- def __iter__(self) -> Iterator[FoamFieldFile]:
77
- for p in self.path.iterdir():
78
- if p.is_file() and (
79
- p.suffix != ".gz" or not p.with_suffix("").is_file()
80
- ):
81
- yield FoamFieldFile(p)
82
-
83
- def __len__(self) -> int:
84
- return len(list(iter(self)))
85
-
86
- def __fspath__(self) -> str:
87
- return str(self.path)
88
-
89
- def __repr__(self) -> str:
90
- return f"{type(self).__qualname__}('{self.path}')"
91
-
92
- def __str__(self) -> str:
93
- return str(self.path)
94
-
95
- @property
96
- def _times(self) -> Sequence["FoamCaseBase.TimeDirectory"]:
97
- times = []
98
- for p in self.path.iterdir():
99
- if p.is_dir():
100
- try:
101
- float(p.name)
102
- except ValueError:
103
- pass
104
- else:
105
- times.append(FoamCaseBase.TimeDirectory(p))
106
-
107
- times.sort(key=lambda t: t.time)
108
-
109
- return times
110
-
111
- @overload
112
- def __getitem__(
113
- self, index: Union[int, float, str]
114
- ) -> "FoamCaseBase.TimeDirectory": ...
115
-
116
- @overload
117
- def __getitem__(self, index: slice) -> Sequence["FoamCaseBase.TimeDirectory"]: ...
118
-
119
- def __getitem__(
120
- self, index: Union[int, slice, float, str]
121
- ) -> Union["FoamCaseBase.TimeDirectory", Sequence["FoamCaseBase.TimeDirectory"]]:
122
- if isinstance(index, str):
123
- return FoamCaseBase.TimeDirectory(self.path / index)
124
- elif isinstance(index, float):
125
- for time in self._times:
126
- if time.time == index:
127
- return time
128
- raise IndexError(f"Time {index} not found")
129
- return self._times[index]
130
-
131
- def __len__(self) -> int:
132
- return len(self._times)
133
-
134
- def _clean_paths(self) -> Set[Path]:
135
- has_decompose_par_dict = (self.path / "system" / "decomposeParDict").is_file()
136
- has_block_mesh_dict = (self.path / "system" / "blockMeshDict").is_file()
137
-
138
- paths = set()
139
-
140
- for p in self.path.iterdir():
141
- if p.is_dir():
142
- try:
143
- t = float(p.name)
144
- except ValueError:
145
- pass
146
- else:
147
- if t != 0:
148
- paths.add(p)
149
-
150
- if has_decompose_par_dict and p.name.startswith("processor"):
151
- paths.add(p)
152
-
153
- if (self.path / "0.orig").is_dir() and (self.path / "0").is_dir():
154
- paths.add(self.path / "0")
155
-
156
- if has_block_mesh_dict and (self.path / "constant" / "polyMesh").exists():
157
- paths.add(self.path / "constant" / "polyMesh")
158
-
159
- if self._run_script() is not None:
160
- paths.update(self.path.glob("log.*"))
161
-
162
- return paths
163
-
164
- def _clone_ignore(
165
- self,
166
- ) -> Callable[[Union[Path, str], Collection[str]], Collection[str]]:
167
- clean_paths = self._clean_paths()
168
-
169
- def ignore(path: Union[Path, str], names: Collection[str]) -> Collection[str]:
170
- paths = {Path(path) / name for name in names}
171
- return {p.name for p in paths.intersection(clean_paths)}
172
-
173
- return ignore
174
-
175
- def _clean_script(self) -> Optional[Path]:
176
- """Return the path to the (All)clean script, or None if no clean script is found."""
177
- clean = self.path / "clean"
178
- all_clean = self.path / "Allclean"
179
-
180
- if clean.is_file():
181
- return clean
182
- elif all_clean.is_file():
183
- return all_clean
184
- else:
185
- return None
186
-
187
- def _run_script(self, *, parallel: Optional[bool] = None) -> Optional[Path]:
188
- """Return the path to the (All)run script, or None if no run script is found."""
189
- run = self.path / "run"
190
- run_parallel = self.path / "run-parallel"
191
- all_run = self.path / "Allrun"
192
- all_run_parallel = self.path / "Allrun-parallel"
193
-
194
- if run.is_file() or all_run.is_file():
195
- if run_parallel.is_file() or all_run_parallel.is_file():
196
- if parallel:
197
- return run_parallel if run_parallel.is_file() else all_run_parallel
198
- elif parallel is False:
199
- return run if run.is_file() else all_run
200
- else:
201
- raise ValueError(
202
- "Both (All)run and (All)run-parallel scripts are present. Please specify parallel argument."
203
- )
204
- return run if run.is_file() else all_run
205
- elif parallel is not False and (
206
- run_parallel.is_file() or all_run_parallel.is_file()
207
- ):
208
- return run_parallel if run_parallel.is_file() else all_run_parallel
209
- else:
210
- return None
211
-
212
- def _parallel_cmd(
213
- self, cmd: Union[Sequence[Union[str, Path]], str, Path]
214
- ) -> Union[Sequence[Union[str, Path]], str]:
215
- if not is_sequence(cmd):
216
- return f"mpiexec -np {self._nprocessors} {cmd} -parallel"
217
- else:
218
- return [
219
- "mpiexec",
220
- "-np",
221
- str(self._nprocessors),
222
- cmd[0],
223
- "-parallel",
224
- *cmd[1:],
225
- ]
226
-
227
- @property
228
- def name(self) -> str:
229
- """The name of the case."""
230
- return self.path.name
231
-
232
- def file(self, path: Union[Path, str]) -> FoamFile:
233
- """Return a FoamFile object for the given path in the case."""
234
- return FoamFile(self.path / path)
235
-
236
- @property
237
- def _nsubdomains(self) -> Optional[int]:
238
- """Return the number of subdomains as set in the decomposeParDict, or None if no decomposeParDict is found."""
239
- try:
240
- nsubdomains = self.decompose_par_dict["numberOfSubdomains"]
241
- if not isinstance(nsubdomains, int):
242
- raise TypeError(
243
- f"numberOfSubdomains in {self.decompose_par_dict} is not an integer"
244
- )
245
- return nsubdomains
246
- except FileNotFoundError:
247
- return None
248
-
249
- @property
250
- def _nprocessors(self) -> int:
251
- """Return the number of processor directories in the case."""
252
- return len(list(self.path.glob("processor*")))
253
-
254
- @property
255
- def application(self) -> str:
256
- """The application name as set in the controlDict."""
257
- application = self.control_dict["application"]
258
- if not isinstance(application, str):
259
- raise TypeError(f"application in {self.control_dict} is not a string")
260
- return application
261
-
262
- @property
263
- def control_dict(self) -> FoamFile:
264
- """The controlDict file."""
265
- return self.file("system/controlDict")
266
-
267
- @property
268
- def fv_schemes(self) -> FoamFile:
269
- """The fvSchemes file."""
270
- return self.file("system/fvSchemes")
271
-
272
- @property
273
- def fv_solution(self) -> FoamFile:
274
- """The fvSolution file."""
275
- return self.file("system/fvSolution")
276
-
277
- @property
278
- def decompose_par_dict(self) -> FoamFile:
279
- """The decomposeParDict file."""
280
- return self.file("system/decomposeParDict")
281
-
282
- @property
283
- def block_mesh_dict(self) -> FoamFile:
284
- """The blockMeshDict file."""
285
- return self.file("system/blockMeshDict")
286
-
287
- @property
288
- def transport_properties(self) -> FoamFile:
289
- """The transportProperties file."""
290
- return self.file("constant/transportProperties")
291
-
292
- @property
293
- def turbulence_properties(self) -> FoamFile:
294
- """The turbulenceProperties file."""
295
- return self.file("constant/turbulenceProperties")
296
-
297
- def __fspath__(self) -> str:
298
- return str(self.path)
299
-
300
- def __repr__(self) -> str:
301
- return f"{type(self).__qualname__}('{self.path}')"
302
-
303
- def __str__(self) -> str:
304
- return str(self.path)
305
-
306
-
307
- class FoamCase(FoamCaseBase):
308
- """
309
- An OpenFOAM case.
310
-
311
- Provides methods for running and cleaning cases, as well as accessing files.
312
-
313
- Access the time directories of the case as a sequence, e.g. `case[0]` or `case[-1]`.
314
-
315
- :param path: The path to the case directory.
316
- """
317
-
318
- def clean(
319
- self,
320
- *,
321
- script: bool = True,
322
- check: bool = False,
323
- ) -> None:
324
- """
325
- Clean this case.
326
-
327
- :param script: If True, use an (All)clean script if it exists. If False, ignore any clean scripts.
328
- :param check: If True, raise a CalledProcessError if the clean script returns a non-zero exit code.
329
- """
330
- script_path = self._clean_script() if script else None
331
-
332
- if script_path is not None:
333
- self.run([script_path], check=check)
334
- else:
335
- for p in self._clean_paths():
336
- if p.is_dir():
337
- shutil.rmtree(p)
338
- else:
339
- p.unlink()
340
-
341
- def run(
342
- self,
343
- cmd: Optional[Union[Sequence[Union[str, Path]], str, Path]] = None,
344
- *,
345
- script: bool = True,
346
- parallel: Optional[bool] = None,
347
- check: bool = True,
348
- ) -> None:
349
- """
350
- Run this case, or a specified command in the context of this case.
351
-
352
- :param cmd: The command to run. If None, run the case. If a sequence, the first element is the command and the rest are arguments. If a string, `cmd` is executed in a shell.
353
- :param script: If True and `cmd` is None, use an (All)run(-parallel) script if it exists for running the case. If False or no run script is found, autodetermine the command(s) needed to run the case.
354
- :param parallel: If True, run in parallel using MPI. If None, autodetect whether to run in parallel.
355
- :param check: If True, raise a CalledProcessError if any command returns a non-zero exit code.
356
- """
357
- if cmd is not None:
358
- if parallel:
359
- cmd = self._parallel_cmd(cmd)
360
-
361
- run_process(
362
- cmd,
363
- check=check,
364
- cwd=self.path,
365
- )
366
- else:
367
- script_path = self._run_script(parallel=parallel) if script else None
368
-
369
- if script_path is not None:
370
- return self.run([script_path], check=check)
371
-
372
- else:
373
- if not self and (self.path / "0.orig").is_dir():
374
- self.restore_0_dir()
375
-
376
- if (self.path / "system" / "blockMeshDict").is_file():
377
- self.block_mesh()
378
-
379
- if parallel is None:
380
- parallel = (
381
- self._nprocessors > 0
382
- or (self.path / "system" / "decomposeParDict").is_file()
383
- )
384
-
385
- if parallel:
386
- if (
387
- self._nprocessors == 0
388
- and (self.path / "system" / "decomposeParDict").is_file()
389
- ):
390
- self.decompose_par()
391
-
392
- self.run(
393
- [self.application],
394
- parallel=parallel,
395
- check=check,
396
- )
397
-
398
- def block_mesh(self, *, check: bool = True) -> None:
399
- """Run blockMesh on this case."""
400
- self.run(["blockMesh"], check=check)
401
-
402
- def decompose_par(self, *, check: bool = True) -> None:
403
- """Decompose this case for parallel running."""
404
- self.run(["decomposePar"], check=check)
405
-
406
- def reconstruct_par(self, *, check: bool = True) -> None:
407
- """Reconstruct this case after parallel running."""
408
- self.run(["reconstructPar"], check=check)
409
-
410
- def restore_0_dir(self) -> None:
411
- """Restore the 0 directory from the 0.orig directory."""
412
- shutil.rmtree(self.path / "0", ignore_errors=True)
413
- shutil.copytree(self.path / "0.orig", self.path / "0")
414
-
415
- def copy(self, dest: Union[Path, str]) -> "FoamCase":
416
- """
417
- Make a copy of this case.
418
-
419
- :param dest: The destination path.
420
- """
421
- return FoamCase(shutil.copytree(self.path, dest, symlinks=True))
422
-
423
- def clone(self, dest: Union[Path, str]) -> "FoamCase":
424
- """
425
- Clone this case (make a clean copy).
426
-
427
- :param dest: The destination path.
428
- """
429
- if self._clean_script() is not None:
430
- copy = self.copy(dest)
431
- copy.clean()
432
- return copy
433
-
434
- dest = Path(dest)
435
-
436
- shutil.copytree(self.path, dest, symlinks=True, ignore=self._clone_ignore())
437
-
438
- return FoamCase(dest)
439
-
440
-
441
- class AsyncFoamCase(FoamCaseBase):
442
- """
443
- An OpenFOAM case with asynchronous support.
444
-
445
- Provides methods for running and cleaning cases, as well as accessing files.
446
-
447
- Access the time directories of the case as a sequence, e.g. `case[0]` or `case[-1]`.
448
-
449
- :param path: The path to the case directory.
450
- """
451
-
452
- max_cpus = multiprocessing.cpu_count()
453
- """
454
- Maximum number of CPUs to use for running `AsyncFoamCase`s concurrently. Defaults to the number of CPUs on the system.
455
- """
456
-
457
- _reserved_cpus = 0
458
- _cpus_cond = None # Cannot be initialized here yet
459
-
460
- @staticmethod
461
- @asynccontextmanager
462
- async def _cpus(cpus: int) -> AsyncGenerator[None, None]:
463
- if AsyncFoamCase._cpus_cond is None:
464
- AsyncFoamCase._cpus_cond = asyncio.Condition()
465
-
466
- cpus = min(cpus, AsyncFoamCase.max_cpus)
467
- if cpus > 0:
468
- async with AsyncFoamCase._cpus_cond:
469
- await AsyncFoamCase._cpus_cond.wait_for(
470
- lambda: AsyncFoamCase.max_cpus - AsyncFoamCase._reserved_cpus
471
- >= cpus
472
- )
473
- AsyncFoamCase._reserved_cpus += cpus
474
- try:
475
- yield
476
- finally:
477
- if cpus > 0:
478
- async with AsyncFoamCase._cpus_cond:
479
- AsyncFoamCase._reserved_cpus -= cpus
480
- AsyncFoamCase._cpus_cond.notify(cpus)
481
-
482
- async def clean(
483
- self,
484
- *,
485
- script: bool = True,
486
- check: bool = False,
487
- ) -> None:
488
- """
489
- Clean this case.
490
-
491
- :param script: If True, use an (All)clean script if it exists. If False, ignore any clean scripts.
492
- :param check: If True, raise a CalledProcessError if the clean script returns a non-zero exit code.
493
- """
494
- script_path = self._clean_script() if script else None
495
-
496
- if script_path is not None:
497
- await self.run([script_path], check=check)
498
- else:
499
- for p in self._clean_paths():
500
- if p.is_dir():
501
- await aioshutil.rmtree(p) # type: ignore [call-arg]
502
- else:
503
- p.unlink()
504
-
505
- async def run(
506
- self,
507
- cmd: Optional[Union[Sequence[Union[str, Path]], str, Path]] = None,
508
- *,
509
- script: bool = True,
510
- parallel: Optional[bool] = None,
511
- cpus: Optional[int] = None,
512
- check: bool = True,
513
- ) -> None:
514
- """
515
- Run this case, or a specified command in the context of this case.
516
-
517
- :param cmd: The command to run. If None, run the case. If a sequence, the first element is the command and the rest are arguments. If a string, `cmd` is executed in a shell.
518
- :param script: If True and `cmd` is None, use an (All)run(-parallel) script if it exists for running the case. If False or no run script is found, autodetermine the command(s) needed to run the case.
519
- :param parallel: If True, run in parallel using MPI. If None, autodetect whether to run in parallel.
520
- :param cpus: The number of CPUs to reserve for the run. The run will wait until the requested number of CPUs is available. If None, autodetect the number of CPUs to reserve.
521
- :param check: If True, raise a CalledProcessError if a command returns a non-zero exit code.
522
- """
523
- if cmd is not None:
524
- if cpus is None:
525
- if parallel:
526
- cpus = min(self._nprocessors, 1)
527
- else:
528
- cpus = 1
529
-
530
- if parallel:
531
- cmd = self._parallel_cmd(cmd)
532
-
533
- async with self._cpus(cpus):
534
- await run_process_async(
535
- cmd,
536
- check=check,
537
- cwd=self.path,
538
- )
539
- else:
540
- script_path = self._run_script(parallel=parallel) if script else None
541
-
542
- if script_path is not None:
543
- if cpus is None:
544
- if self._nprocessors > 0:
545
- cpus = self._nprocessors
546
- else:
547
- nsubdomains = self._nsubdomains
548
- if nsubdomains is not None:
549
- cpus = nsubdomains
550
- else:
551
- cpus = 1
552
-
553
- await self.run([script_path], check=check, cpus=cpus)
554
-
555
- else:
556
- if not self and (self.path / "0.orig").is_dir():
557
- await self.restore_0_dir()
558
-
559
- if (self.path / "system" / "blockMeshDict").is_file():
560
- await self.block_mesh()
561
-
562
- if parallel is None:
563
- parallel = (
564
- self._nprocessors > 0
565
- or (self.path / "system" / "decomposeParDict").is_file()
566
- )
567
-
568
- if parallel:
569
- if (
570
- self._nprocessors == 0
571
- and (self.path / "system" / "decomposeParDict").is_file()
572
- ):
573
- await self.decompose_par()
574
-
575
- if cpus is None:
576
- cpus = min(self._nprocessors, 1)
577
- else:
578
- if cpus is None:
579
- cpus = 1
580
-
581
- await self.run(
582
- [self.application],
583
- parallel=parallel,
584
- check=check,
585
- cpus=cpus,
586
- )
587
-
588
- async def block_mesh(self, *, check: bool = True) -> None:
589
- """Run blockMesh on this case."""
590
- await self.run(["blockMesh"], check=check)
591
-
592
- async def decompose_par(self, *, check: bool = True) -> None:
593
- """Decompose this case for parallel running."""
594
- await self.run(["decomposePar"], check=check)
595
-
596
- async def reconstruct_par(self, *, check: bool = True) -> None:
597
- """Reconstruct this case after parallel running."""
598
- await self.run(["reconstructPar"], check=check)
599
-
600
- async def restore_0_dir(self) -> None:
601
- """Restore the 0 directory from the 0.orig directory."""
602
- await aioshutil.rmtree(self.path / "0", ignore_errors=True) # type: ignore [call-arg]
603
- await aioshutil.copytree(self.path / "0.orig", self.path / "0")
604
-
605
- async def copy(self, dest: Union[Path, str]) -> "AsyncFoamCase":
606
- """
607
- Make a copy of this case.
608
-
609
- :param dest: The destination path.
610
- """
611
- return AsyncFoamCase(await aioshutil.copytree(self.path, dest, symlinks=True))
612
-
613
- async def clone(self, dest: Union[Path, str]) -> "AsyncFoamCase":
614
- """
615
- Clone this case (make a clean copy).
616
-
617
- :param dest: The destination path.
618
- """
619
- if self._clean_script() is not None:
620
- copy = await self.copy(dest)
621
- await copy.clean()
622
- return copy
623
-
624
- dest = Path(dest)
625
-
626
- await aioshutil.copytree(
627
- self.path, dest, symlinks=True, ignore=self._clone_ignore()
628
- )
629
-
630
- return AsyncFoamCase(dest)
@@ -1,15 +0,0 @@
1
- foamlib/__init__.py,sha256=m4p7yGjpDi2csSsHt9gXaWEWSPqGqHJjCeawUBzmNUM,432
2
- foamlib/_cases.py,sha256=LK2EvZTAOejzDU7t2Vgsn7WG9AEdFoQwv5ifjWbyP2Y,21471
3
- foamlib/_util.py,sha256=vL03aAzpWdZyYIhe2WTxHiz9b4lnttVnRuzqUmZvXXk,3047
4
- foamlib/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- foamlib/_files/__init__.py,sha256=vDkPj8u8bX_I_m2YfeKvXBgwg8D1ufyFCfHGHKN3JPQ,140
6
- foamlib/_files/_base.py,sha256=YA5a-i5HZuA3JslCD6r-DwZzpSA8r42dqSXef286Ako,2050
7
- foamlib/_files/_files.py,sha256=4rZb3HMJADmJBGmRpChiujJNX1UIxhgHI5YPEmEOWvE,10784
8
- foamlib/_files/_io.py,sha256=pGYMoLI5Dz2Y90EB1CtklZiffpDTGuQbbbkyqRe0JAo,2115
9
- foamlib/_files/_parsing.py,sha256=SW1c1adDTg3e65hhZv5ZFDX8j654E88FDn13zl8gJ8c,7694
10
- foamlib/_files/_serialization.py,sha256=LCeaLWtNvkcs0dfowL7nViiByxw7U_fvgueVjFliipU,3462
11
- foamlib-0.3.11.dist-info/LICENSE.txt,sha256=5Dte9TUnLZzPRs4NQzl-Jc2-Ljd-t_v0ZR5Ng5r0UsY,35131
12
- foamlib-0.3.11.dist-info/METADATA,sha256=qHXzHhl0N-Sf1igVKDTgMUpYhlqYiIl9FyGEOjnWsFM,5458
13
- foamlib-0.3.11.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
14
- foamlib-0.3.11.dist-info/top_level.txt,sha256=ZdVYtetXGwPwyfL-WhlhbTFQGAwKX5P_gXxtH9JYFPI,8
15
- foamlib-0.3.11.dist-info/RECORD,,