foamlib 0.4.4__py3-none-any.whl → 0.5.0__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,11 +1,10 @@
1
1
  """A Python interface for interacting with OpenFOAM."""
2
2
 
3
- __version__ = "0.4.4"
3
+ __version__ = "0.5.0"
4
4
 
5
5
  from ._cases import (
6
6
  AsyncFoamCase,
7
7
  CalledProcessError,
8
- CalledProcessWarning,
9
8
  FoamCase,
10
9
  FoamCaseBase,
11
10
  )
@@ -19,5 +18,4 @@ __all__ = [
19
18
  "FoamFieldFile",
20
19
  "FoamFileBase",
21
20
  "CalledProcessError",
22
- "CalledProcessWarning",
23
21
  ]
@@ -1,12 +1,11 @@
1
1
  from ._async import AsyncFoamCase
2
2
  from ._base import FoamCaseBase
3
+ from ._subprocess import CalledProcessError
3
4
  from ._sync import FoamCase
4
- from ._util import CalledProcessError, CalledProcessWarning
5
5
 
6
6
  __all__ = [
7
7
  "FoamCaseBase",
8
8
  "FoamCase",
9
9
  "AsyncFoamCase",
10
10
  "CalledProcessError",
11
- "CalledProcessWarning",
12
11
  ]
foamlib/_cases/_async.py CHANGED
@@ -1,6 +1,8 @@
1
1
  import asyncio
2
2
  import multiprocessing
3
+ import os
3
4
  import sys
5
+ import tempfile
4
6
  from contextlib import asynccontextmanager
5
7
  from pathlib import Path
6
8
  from typing import (
@@ -14,14 +16,19 @@ if sys.version_info >= (3, 9):
14
16
  else:
15
17
  from typing import AsyncGenerator, Collection, Sequence
16
18
 
19
+ if sys.version_info >= (3, 11):
20
+ from typing import Self
21
+ else:
22
+ from typing_extensions import Self
23
+
17
24
  import aioshutil
18
25
 
19
- from .._util import is_sequence
20
- from ._base import FoamCaseBase
21
- from ._util import check_returncode
26
+ from ._recipes import _FoamCaseRecipes
27
+ from ._subprocess import run_async
28
+ from ._util import awaitableasynccontextmanager
22
29
 
23
30
 
24
- class AsyncFoamCase(FoamCaseBase):
31
+ class AsyncFoamCase(_FoamCaseRecipes):
25
32
  """
26
33
  An OpenFOAM case with asynchronous support.
27
34
 
@@ -63,17 +70,19 @@ class AsyncFoamCase(FoamCaseBase):
63
70
  AsyncFoamCase._cpus_cond.notify(cpus)
64
71
 
65
72
  @staticmethod
66
- async def _rmtree(path: Path, ignore_errors: bool = False) -> None:
73
+ async def _rmtree(
74
+ path: Union["os.PathLike[str]", str], ignore_errors: bool = False
75
+ ) -> None:
67
76
  await aioshutil.rmtree(path, ignore_errors=ignore_errors) # type: ignore [call-arg]
68
77
 
69
78
  @staticmethod
70
79
  async def _copytree(
71
- src: Path,
72
- dest: Path,
80
+ src: Union["os.PathLike[str]", str],
81
+ dest: Union["os.PathLike[str]", str],
73
82
  *,
74
83
  symlinks: bool = False,
75
84
  ignore: Optional[
76
- Callable[[Union[Path, str], Collection[str]], Collection[str]]
85
+ Callable[[Union["os.PathLike[str]", str], Collection[str]], Collection[str]]
77
86
  ] = None,
78
87
  ) -> None:
79
88
  await aioshutil.copytree(src, dest, symlinks=symlinks, ignore=ignore)
@@ -95,60 +104,54 @@ class AsyncFoamCase(FoamCaseBase):
95
104
 
96
105
  async def _run(
97
106
  self,
98
- cmd: Union[Sequence[Union[str, Path]], str, Path],
107
+ cmd: Union[Sequence[Union[str, "os.PathLike[str]"]], str],
99
108
  *,
100
109
  parallel: bool = False,
101
110
  cpus: int = 1,
102
111
  check: bool = True,
112
+ log: bool = True,
103
113
  ) -> None:
104
- async with self._cpus(cpus):
105
- if not is_sequence(cmd):
106
- if parallel:
107
- cmd = f"mpiexec -np {cpus} {cmd} -parallel"
108
-
109
- proc = await asyncio.create_subprocess_shell(
110
- str(cmd),
114
+ with self._output(cmd, log=log) as (stdout, stderr):
115
+ if parallel:
116
+ if isinstance(cmd, str):
117
+ cmd = [
118
+ "mpiexec",
119
+ "-n",
120
+ str(cpus),
121
+ "/bin/sh",
122
+ "-c",
123
+ f"{cmd} -parallel",
124
+ ]
125
+ else:
126
+ cmd = ["mpiexec", "-n", str(cpus), *cmd, "-parallel"]
127
+
128
+ async with self._cpus(cpus):
129
+ await run_async(
130
+ cmd,
131
+ check=check,
111
132
  cwd=self.path,
112
- env=self._env(shell=True),
113
- stdout=asyncio.subprocess.DEVNULL,
114
- stderr=asyncio.subprocess.PIPE
115
- if check
116
- else asyncio.subprocess.DEVNULL,
133
+ env=self._env(shell=isinstance(cmd, str)),
134
+ stdout=stdout,
135
+ stderr=stderr,
117
136
  )
118
137
 
119
- else:
120
- if parallel:
121
- cmd = ["mpiexec", "-np", str(cpus), *cmd, "-parallel"]
122
-
123
- if sys.version_info < (3, 8):
124
- cmd = (str(arg) for arg in cmd)
125
- proc = await asyncio.create_subprocess_exec(
126
- *cmd,
127
- cwd=self.path,
128
- env=self._env(shell=False),
129
- stdout=asyncio.subprocess.DEVNULL,
130
- stderr=asyncio.subprocess.PIPE
131
- if check
132
- else asyncio.subprocess.DEVNULL,
133
- )
134
-
135
- stdout, stderr = await proc.communicate()
136
-
137
- assert stdout is None
138
- assert proc.returncode is not None
139
-
140
- if check:
141
- check_returncode(proc.returncode, cmd, stderr.decode())
142
-
143
138
  async def run(
144
139
  self,
145
- cmd: Optional[Union[Sequence[Union[str, Path]], str, Path]] = None,
140
+ cmd: Optional[Union[Sequence[Union[str, "os.PathLike[str]"]], str]] = None,
146
141
  *,
147
142
  script: bool = True,
148
143
  parallel: Optional[bool] = None,
149
144
  cpus: Optional[int] = None,
150
145
  check: bool = True,
151
146
  ) -> None:
147
+ """
148
+ Run this case, or a specified command in the context of this case.
149
+
150
+ :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.
151
+ :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.
152
+ :param parallel: If True, run in parallel using MPI. If None, autodetect whether to run in parallel.
153
+ :param check: If True, raise a CalledProcessError if any command returns a non-zero exit code.
154
+ """
152
155
  for name, args, kwargs in self._run_cmds(
153
156
  cmd=cmd, script=script, parallel=parallel, cpus=cpus, check=check
154
157
  ):
@@ -171,24 +174,60 @@ class AsyncFoamCase(FoamCaseBase):
171
174
  for name, args, kwargs in self._restore_0_dir_cmds():
172
175
  await getattr(self, name)(*args, **kwargs)
173
176
 
174
- async def copy(self, dest: Union[Path, str]) -> "AsyncFoamCase":
177
+ @awaitableasynccontextmanager
178
+ @asynccontextmanager
179
+ async def copy(
180
+ self, dst: Optional[Union["os.PathLike[str]", str]] = None
181
+ ) -> "AsyncGenerator[Self]":
175
182
  """
176
183
  Make a copy of this case.
177
184
 
178
- :param dest: The destination path.
185
+ Use as an async context manager to automatically delete the copy when done.
186
+
187
+ :param dst: The destination path. If None, copy to a temporary directory.
179
188
  """
180
- for name, args, kwargs in self._copy_cmds(dest):
189
+ if dst is None:
190
+ dst = Path(tempfile.mkdtemp(), self.name)
191
+ tmp = True
192
+ else:
193
+ tmp = False
194
+
195
+ for name, args, kwargs in self._copy_cmds(dst):
181
196
  await getattr(self, name)(*args, **kwargs)
182
197
 
183
- return AsyncFoamCase(dest)
198
+ yield type(self)(dst)
184
199
 
185
- async def clone(self, dest: Union[Path, str]) -> "AsyncFoamCase":
200
+ if tmp:
201
+ assert isinstance(dst, Path)
202
+ await self._rmtree(dst.parent)
203
+ else:
204
+ await self._rmtree(dst)
205
+
206
+ @awaitableasynccontextmanager
207
+ @asynccontextmanager
208
+ async def clone(
209
+ self, dst: Optional[Union["os.PathLike[str]", str]] = None
210
+ ) -> "AsyncGenerator[Self]":
186
211
  """
187
212
  Clone this case (make a clean copy).
188
213
 
189
- :param dest: The destination path.
214
+ Use as an async context manager to automatically delete the clone when done.
215
+
216
+ :param dst: The destination path. If None, clone to a temporary directory.
190
217
  """
191
- for name, args, kwargs in self._clone_cmds(dest):
218
+ if dst is None:
219
+ dst = Path(tempfile.mkdtemp(), self.name)
220
+ tmp = True
221
+ else:
222
+ tmp = False
223
+
224
+ for name, args, kwargs in self._clone_cmds(dst):
192
225
  await getattr(self, name)(*args, **kwargs)
193
226
 
194
- return AsyncFoamCase(dest)
227
+ yield type(self)(dst)
228
+
229
+ if tmp:
230
+ assert isinstance(dst, Path)
231
+ await self._rmtree(dst.parent)
232
+ else:
233
+ await self._rmtree(dst)
foamlib/_cases/_base.py CHANGED
@@ -3,40 +3,29 @@ import shutil
3
3
  import sys
4
4
  from pathlib import Path
5
5
  from typing import (
6
- Any,
7
6
  Optional,
8
- Tuple,
9
7
  Union,
10
8
  overload,
11
9
  )
12
10
 
13
11
  if sys.version_info >= (3, 9):
14
12
  from collections.abc import (
15
- Callable,
16
- Collection,
17
- Generator,
18
13
  Iterator,
19
- Mapping,
20
14
  Sequence,
21
15
  Set,
22
16
  )
23
17
  else:
24
18
  from typing import AbstractSet as Set
25
19
  from typing import (
26
- Callable,
27
- Collection,
28
- Generator,
29
20
  Iterator,
30
- Mapping,
31
21
  Sequence,
32
22
  )
33
23
 
34
-
35
24
  from .._files import FoamFieldFile, FoamFile
36
25
 
37
26
 
38
27
  class FoamCaseBase(Sequence["FoamCaseBase.TimeDirectory"]):
39
- def __init__(self, path: Union[Path, str, "FoamCaseBase"] = Path()):
28
+ def __init__(self, path: Union["os.PathLike[str]", str] = Path()):
40
29
  self.path = Path(path).absolute()
41
30
 
42
31
  class TimeDirectory(Set[FoamFieldFile]):
@@ -48,7 +37,7 @@ class FoamCaseBase(Sequence["FoamCaseBase.TimeDirectory"]):
48
37
  :param path: The path to the time directory.
49
38
  """
50
39
 
51
- def __init__(self, path: Union[Path, str]):
40
+ def __init__(self, path: Union["os.PathLike[str]", str]):
52
41
  self.path = Path(path).absolute()
53
42
 
54
43
  @property
@@ -141,254 +130,15 @@ class FoamCaseBase(Sequence["FoamCaseBase.TimeDirectory"]):
141
130
  def __len__(self) -> int:
142
131
  return len(self._times)
143
132
 
144
- def _clean_paths(self) -> Set[Path]:
145
- has_decompose_par_dict = (self.path / "system" / "decomposeParDict").is_file()
146
- has_block_mesh_dict = (self.path / "system" / "blockMeshDict").is_file()
147
-
148
- paths = set()
149
-
150
- for p in self.path.iterdir():
151
- if p.is_dir():
152
- try:
153
- t = float(p.name)
154
- except ValueError:
155
- pass
156
- else:
157
- if t != 0:
158
- paths.add(p)
159
-
160
- if has_decompose_par_dict and p.name.startswith("processor"):
161
- paths.add(p)
162
-
163
- if (self.path / "0.orig").is_dir() and (self.path / "0").is_dir():
164
- paths.add(self.path / "0")
165
-
166
- if has_block_mesh_dict and (self.path / "constant" / "polyMesh").exists():
167
- paths.add(self.path / "constant" / "polyMesh")
168
-
169
- if self._run_script() is not None:
170
- paths.update(self.path.glob("log.*"))
171
-
172
- return paths
173
-
174
133
  def __delitem__(self, key: Union[int, float, str]) -> None:
175
134
  shutil.rmtree(self[key].path)
176
135
 
177
- def _clone_ignore(
178
- self,
179
- ) -> Callable[[Union[Path, str], Collection[str]], Collection[str]]:
180
- clean_paths = self._clean_paths()
181
-
182
- def ignore(path: Union[Path, str], names: Collection[str]) -> Collection[str]:
183
- paths = {Path(path) / name for name in names}
184
- return {p.name for p in paths.intersection(clean_paths)}
185
-
186
- return ignore
187
-
188
- def _clean_script(self) -> Optional[Path]:
189
- """Return the path to the (All)clean script, or None if no clean script is found."""
190
- clean = self.path / "clean"
191
- all_clean = self.path / "Allclean"
192
-
193
- if clean.is_file():
194
- script = clean
195
- elif all_clean.is_file():
196
- script = all_clean
197
- else:
198
- return None
199
-
200
- if sys.argv and Path(sys.argv[0]).absolute() == script.absolute():
201
- return None
202
-
203
- return script if Path(sys.argv[0]).absolute() != script.absolute() else None
204
-
205
- def _run_script(self, *, parallel: Optional[bool] = None) -> Optional[Path]:
206
- """Return the path to the (All)run script, or None if no run script is found."""
207
- run = self.path / "run"
208
- run_parallel = self.path / "run-parallel"
209
- all_run = self.path / "Allrun"
210
- all_run_parallel = self.path / "Allrun-parallel"
211
-
212
- if run.is_file() or all_run.is_file():
213
- if run_parallel.is_file() or all_run_parallel.is_file():
214
- if parallel:
215
- script = (
216
- run_parallel if run_parallel.is_file() else all_run_parallel
217
- )
218
- elif parallel is False:
219
- script = run if run.is_file() else all_run
220
- else:
221
- raise ValueError(
222
- "Both (All)run and (All)run-parallel scripts are present. Please specify parallel argument."
223
- )
224
- else:
225
- script = run if run.is_file() else all_run
226
- elif parallel is not False and (
227
- run_parallel.is_file() or all_run_parallel.is_file()
228
- ):
229
- script = run_parallel if run_parallel.is_file() else all_run_parallel
230
- else:
231
- return None
232
-
233
- if sys.argv and Path(sys.argv[0]).absolute() == script.absolute():
234
- return None
235
-
236
- return script
237
-
238
- def _env(self, *, shell: bool) -> Optional[Mapping[str, str]]:
239
- sip_workaround = os.environ.get(
240
- "FOAM_LD_LIBRARY_PATH", ""
241
- ) and not os.environ.get("DYLD_LIBRARY_PATH", "")
242
-
243
- if not shell or sip_workaround:
244
- env = os.environ.copy()
245
-
246
- if not shell:
247
- env["PWD"] = str(self.path)
248
-
249
- if sip_workaround:
250
- env["DYLD_LIBRARY_PATH"] = env["FOAM_LD_LIBRARY_PATH"]
251
-
252
- return env
253
- else:
254
- return None
255
-
256
- def _copy_cmds(
257
- self, dest: Union[Path, str]
258
- ) -> Generator[Tuple[str, Sequence[Any], Mapping[str, Any]], None, None]:
259
- yield (
260
- "_copytree",
261
- (
262
- self.path,
263
- dest,
264
- ),
265
- {"symlinks": True},
266
- )
267
-
268
- def _clean_cmds(
269
- self, *, script: bool = True, check: bool = False
270
- ) -> Generator[Tuple[str, Sequence[Any], Mapping[str, Any]], None, None]:
271
- script_path = self._clean_script() if script else None
272
-
273
- if script_path is not None:
274
- yield ("_run", ([script_path],), {"cpus": 0, "check": check})
275
- else:
276
- for p in self._clean_paths():
277
- if p.is_dir():
278
- yield ("_rmtree", (p,), {})
279
- else:
280
- p.unlink()
281
-
282
- def _clone_cmds(
283
- self, dest: Union[Path, str]
284
- ) -> Generator[Tuple[str, Sequence[Any], Mapping[str, Any]], None, None]:
285
- if self._clean_script() is not None:
286
- yield ("copy", (dest,), {})
287
- yield ("clean", (), {})
288
- else:
289
- yield (
290
- "_copytree",
291
- (
292
- self.path,
293
- dest,
294
- ),
295
- {"symlinks": True, "ignore": self._clone_ignore()},
296
- )
297
-
298
- def _restore_0_dir_cmds(
299
- self,
300
- ) -> Generator[Tuple[str, Sequence[Any], Mapping[str, Any]], None, None]:
301
- yield ("_rmtree", (self.path / "0",), {"ignore_errors": True})
302
- yield (
303
- "_copytree",
304
- (
305
- self.path / "0.orig",
306
- self.path / "0",
307
- ),
308
- {"symlinks": True},
309
- )
310
-
311
- def _run_cmds(
312
- self,
313
- cmd: Optional[Union[Sequence[Union[str, Path]], str, Path]] = None,
314
- *,
315
- script: bool = True,
316
- parallel: Optional[bool] = None,
317
- cpus: Optional[int] = None,
318
- check: bool = True,
319
- ) -> Generator[Tuple[str, Sequence[Any], Mapping[str, Any]], None, None]:
320
- if cmd is not None:
321
- if parallel:
322
- if cpus is None:
323
- cpus = max(self._nprocessors, 1)
324
- else:
325
- parallel = False
326
- if cpus is None:
327
- cpus = 1
328
-
329
- yield ("_run", (cmd,), {"parallel": parallel, "cpus": cpus, "check": check})
330
-
331
- else:
332
- script_path = self._run_script(parallel=parallel) if script else None
333
-
334
- if script_path is not None:
335
- if parallel or parallel is None:
336
- if cpus is None:
337
- if self._nprocessors > 0:
338
- cpus = self._nprocessors
339
- elif (self.path / "system" / "decomposeParDict").is_file():
340
- cpus = self._nsubdomains
341
- else:
342
- cpus = 1
343
- else:
344
- if cpus is None:
345
- cpus = 1
346
-
347
- yield (
348
- "_run",
349
- ([script_path],),
350
- {"parallel": False, "cpus": cpus, "check": check},
351
- )
352
-
353
- else:
354
- if not self and (self.path / "0.orig").is_dir():
355
- yield ("restore_0_dir", (), {})
356
-
357
- if (self.path / "system" / "blockMeshDict").is_file():
358
- yield ("block_mesh", (), {"check": check})
359
-
360
- if parallel is None:
361
- parallel = (
362
- (cpus is not None and cpus > 1)
363
- or self._nprocessors > 0
364
- or (self.path / "system" / "decomposeParDict").is_file()
365
- )
366
-
367
- if parallel:
368
- if (
369
- self._nprocessors == 0
370
- and (self.path / "system" / "decomposeParDict").is_file()
371
- ):
372
- yield ("decompose_par", (), {"check": check})
373
-
374
- if cpus is None:
375
- cpus = max(self._nprocessors, 1)
376
- else:
377
- if cpus is None:
378
- cpus = 1
379
-
380
- yield (
381
- "_run",
382
- ([self.application],),
383
- {"parallel": parallel, "cpus": cpus, "check": check},
384
- )
385
-
386
136
  @property
387
137
  def name(self) -> str:
388
138
  """The name of the case."""
389
139
  return self.path.name
390
140
 
391
- def file(self, path: Union[Path, str]) -> FoamFile:
141
+ def file(self, path: Union["os.PathLike[str]", str]) -> FoamFile:
392
142
  """Return a FoamFile object for the given path in the case."""
393
143
  return FoamFile(self.path / path)
394
144
 
@@ -0,0 +1,299 @@
1
+ import os
2
+ import shlex
3
+ import shutil
4
+ import sys
5
+ from contextlib import contextmanager
6
+ from pathlib import Path
7
+ from typing import (
8
+ IO,
9
+ Any,
10
+ Optional,
11
+ Tuple,
12
+ Union,
13
+ )
14
+
15
+ if sys.version_info >= (3, 9):
16
+ from collections.abc import (
17
+ Callable,
18
+ Collection,
19
+ Generator,
20
+ Mapping,
21
+ Sequence,
22
+ Set,
23
+ )
24
+ else:
25
+ from typing import AbstractSet as Set
26
+ from typing import (
27
+ Callable,
28
+ Collection,
29
+ Generator,
30
+ Mapping,
31
+ Sequence,
32
+ )
33
+
34
+ from ._base import FoamCaseBase
35
+ from ._subprocess import DEVNULL, STDOUT
36
+
37
+
38
+ class _FoamCaseRecipes(FoamCaseBase):
39
+ def _clean_paths(self) -> Set[Path]:
40
+ has_decompose_par_dict = (self.path / "system" / "decomposeParDict").is_file()
41
+ has_block_mesh_dict = (self.path / "system" / "blockMeshDict").is_file()
42
+
43
+ paths = set()
44
+
45
+ for p in self.path.iterdir():
46
+ if p.is_dir():
47
+ try:
48
+ t = float(p.name)
49
+ except ValueError:
50
+ pass
51
+ else:
52
+ if t != 0:
53
+ paths.add(p)
54
+
55
+ if has_decompose_par_dict and p.name.startswith("processor"):
56
+ paths.add(p)
57
+
58
+ if (self.path / "0.orig").is_dir() and (self.path / "0").is_dir():
59
+ paths.add(self.path / "0")
60
+
61
+ if has_block_mesh_dict and (self.path / "constant" / "polyMesh").exists():
62
+ paths.add(self.path / "constant" / "polyMesh")
63
+
64
+ if self._run_script() is not None:
65
+ paths.update(self.path.glob("log.*"))
66
+
67
+ return paths
68
+
69
+ def __delitem__(self, key: Union[int, float, str]) -> None:
70
+ shutil.rmtree(self[key].path)
71
+
72
+ def _clone_ignore(
73
+ self,
74
+ ) -> Callable[[Union["os.PathLike[str]", str], Collection[str]], Collection[str]]:
75
+ clean_paths = self._clean_paths()
76
+
77
+ def ignore(
78
+ path: Union["os.PathLike[str]", str], names: Collection[str]
79
+ ) -> Collection[str]:
80
+ paths = {Path(path) / name for name in names}
81
+ return {p.name for p in paths.intersection(clean_paths)}
82
+
83
+ return ignore
84
+
85
+ def _clean_script(self) -> Optional[Path]:
86
+ """Return the path to the (All)clean script, or None if no clean script is found."""
87
+ clean = self.path / "clean"
88
+ all_clean = self.path / "Allclean"
89
+
90
+ if clean.is_file():
91
+ script = clean
92
+ elif all_clean.is_file():
93
+ script = all_clean
94
+ else:
95
+ return None
96
+
97
+ if sys.argv and Path(sys.argv[0]).absolute() == script.absolute():
98
+ return None
99
+
100
+ return script if Path(sys.argv[0]).absolute() != script.absolute() else None
101
+
102
+ def _run_script(self, *, parallel: Optional[bool] = None) -> Optional[Path]:
103
+ """Return the path to the (All)run script, or None if no run script is found."""
104
+ run = self.path / "run"
105
+ run_parallel = self.path / "run-parallel"
106
+ all_run = self.path / "Allrun"
107
+ all_run_parallel = self.path / "Allrun-parallel"
108
+
109
+ if run.is_file() or all_run.is_file():
110
+ if run_parallel.is_file() or all_run_parallel.is_file():
111
+ if parallel:
112
+ script = (
113
+ run_parallel if run_parallel.is_file() else all_run_parallel
114
+ )
115
+ elif parallel is False:
116
+ script = run if run.is_file() else all_run
117
+ else:
118
+ raise ValueError(
119
+ "Both (All)run and (All)run-parallel scripts are present. Please specify parallel argument."
120
+ )
121
+ else:
122
+ script = run if run.is_file() else all_run
123
+ elif parallel is not False and (
124
+ run_parallel.is_file() or all_run_parallel.is_file()
125
+ ):
126
+ script = run_parallel if run_parallel.is_file() else all_run_parallel
127
+ else:
128
+ return None
129
+
130
+ if sys.argv and Path(sys.argv[0]).absolute() == script.absolute():
131
+ return None
132
+
133
+ return script
134
+
135
+ def _env(self, *, shell: bool) -> Optional[Mapping[str, str]]:
136
+ sip_workaround = os.environ.get(
137
+ "FOAM_LD_LIBRARY_PATH", ""
138
+ ) and not os.environ.get("DYLD_LIBRARY_PATH", "")
139
+
140
+ if not shell or sip_workaround:
141
+ env = os.environ.copy()
142
+
143
+ if not shell:
144
+ env["PWD"] = str(self.path)
145
+
146
+ if sip_workaround:
147
+ env["DYLD_LIBRARY_PATH"] = env["FOAM_LD_LIBRARY_PATH"]
148
+
149
+ return env
150
+ else:
151
+ return None
152
+
153
+ @contextmanager
154
+ def _output(
155
+ self, cmd: Union[Sequence[Union[str, "os.PathLike[str]"]], str], *, log: bool
156
+ ) -> Generator[Tuple[Union[int, IO[bytes]], Union[int, IO[bytes]]], None, None]:
157
+ if log:
158
+ if isinstance(cmd, str):
159
+ name = shlex.split(cmd)[0]
160
+ else:
161
+ if isinstance(cmd[0], os.PathLike):
162
+ name = Path(cmd[0]).name
163
+ else:
164
+ name = cmd[0]
165
+
166
+ with (self.path / f"log.{name}").open("ab") as stdout:
167
+ yield stdout, STDOUT
168
+ else:
169
+ yield DEVNULL, DEVNULL
170
+
171
+ def _copy_cmds(
172
+ self, dest: Union["os.PathLike[str]", str]
173
+ ) -> Generator[Tuple[str, Sequence[Any], Mapping[str, Any]], None, None]:
174
+ yield (
175
+ "_copytree",
176
+ (
177
+ self.path,
178
+ dest,
179
+ ),
180
+ {"symlinks": True},
181
+ )
182
+
183
+ def _clean_cmds(
184
+ self, *, script: bool = True, check: bool = False
185
+ ) -> Generator[Tuple[str, Sequence[Any], Mapping[str, Any]], None, None]:
186
+ script_path = self._clean_script() if script else None
187
+
188
+ if script_path is not None:
189
+ yield ("_run", ([script_path],), {"cpus": 0, "check": check, "log": False})
190
+ else:
191
+ for p in self._clean_paths():
192
+ if p.is_dir():
193
+ yield ("_rmtree", (p,), {})
194
+ else:
195
+ p.unlink()
196
+
197
+ def _clone_cmds(
198
+ self, dest: Union["os.PathLike[str]", str]
199
+ ) -> Generator[Tuple[str, Sequence[Any], Mapping[str, Any]], None, None]:
200
+ if self._clean_script() is not None:
201
+ yield ("copy", (dest,), {})
202
+ yield ("clean", (), {})
203
+ else:
204
+ yield (
205
+ "_copytree",
206
+ (
207
+ self.path,
208
+ dest,
209
+ ),
210
+ {"symlinks": True, "ignore": self._clone_ignore()},
211
+ )
212
+
213
+ def _restore_0_dir_cmds(
214
+ self,
215
+ ) -> Generator[Tuple[str, Sequence[Any], Mapping[str, Any]], None, None]:
216
+ yield ("_rmtree", (self.path / "0",), {"ignore_errors": True})
217
+ yield (
218
+ "_copytree",
219
+ (
220
+ self.path / "0.orig",
221
+ self.path / "0",
222
+ ),
223
+ {"symlinks": True},
224
+ )
225
+
226
+ def _run_cmds(
227
+ self,
228
+ cmd: Optional[Union[Sequence[Union[str, "os.PathLike[str]"]], str]] = None,
229
+ *,
230
+ script: bool = True,
231
+ parallel: Optional[bool] = None,
232
+ cpus: Optional[int] = None,
233
+ check: bool = True,
234
+ ) -> Generator[Tuple[str, Sequence[Any], Mapping[str, Any]], None, None]:
235
+ if cmd is not None:
236
+ if parallel:
237
+ if cpus is None:
238
+ cpus = max(self._nprocessors, 1)
239
+ else:
240
+ parallel = False
241
+ if cpus is None:
242
+ cpus = 1
243
+
244
+ yield ("_run", (cmd,), {"parallel": parallel, "cpus": cpus, "check": check})
245
+
246
+ else:
247
+ script_path = self._run_script(parallel=parallel) if script else None
248
+
249
+ if script_path is not None:
250
+ if parallel or parallel is None:
251
+ if cpus is None:
252
+ if self._nprocessors > 0:
253
+ cpus = self._nprocessors
254
+ elif (self.path / "system" / "decomposeParDict").is_file():
255
+ cpus = self._nsubdomains
256
+ else:
257
+ cpus = 1
258
+ else:
259
+ if cpus is None:
260
+ cpus = 1
261
+
262
+ yield (
263
+ "_run",
264
+ ([script_path],),
265
+ {"parallel": False, "cpus": cpus, "check": check},
266
+ )
267
+
268
+ else:
269
+ if not self and (self.path / "0.orig").is_dir():
270
+ yield ("restore_0_dir", (), {})
271
+
272
+ if (self.path / "system" / "blockMeshDict").is_file():
273
+ yield ("block_mesh", (), {"check": check})
274
+
275
+ if parallel is None:
276
+ parallel = (
277
+ (cpus is not None and cpus > 1)
278
+ or self._nprocessors > 0
279
+ or (self.path / "system" / "decomposeParDict").is_file()
280
+ )
281
+
282
+ if parallel:
283
+ if (
284
+ self._nprocessors == 0
285
+ and (self.path / "system" / "decomposeParDict").is_file()
286
+ ):
287
+ yield ("decompose_par", (), {"check": check})
288
+
289
+ if cpus is None:
290
+ cpus = max(self._nprocessors, 1)
291
+ else:
292
+ if cpus is None:
293
+ cpus = 1
294
+
295
+ yield (
296
+ "_run",
297
+ ([self.application],),
298
+ {"parallel": parallel, "cpus": cpus, "check": check},
299
+ )
@@ -0,0 +1,86 @@
1
+ import asyncio
2
+ import os
3
+ import subprocess
4
+ import sys
5
+ from typing import IO, Optional, Union
6
+
7
+ if sys.version_info >= (3, 9):
8
+ from collections.abc import Mapping, Sequence
9
+ else:
10
+ from typing import Mapping, Sequence
11
+
12
+ CalledProcessError = subprocess.CalledProcessError
13
+ CompletedProcess = subprocess.CompletedProcess
14
+
15
+ DEVNULL = subprocess.DEVNULL
16
+ PIPE = subprocess.PIPE
17
+ STDOUT = subprocess.STDOUT
18
+
19
+
20
+ def run_sync(
21
+ cmd: Union[Sequence[Union[str, "os.PathLike[str]"]], str],
22
+ *,
23
+ check: bool = True,
24
+ cwd: Optional["os.PathLike[str]"] = None,
25
+ env: Optional[Mapping[str, str]] = None,
26
+ stdout: Optional[Union[int, IO[bytes]]] = None,
27
+ stderr: Optional[Union[int, IO[bytes]]] = None,
28
+ ) -> "CompletedProcess[bytes]":
29
+ if not isinstance(cmd, str) and sys.version_info < (3, 8):
30
+ cmd = [str(arg) for arg in cmd]
31
+
32
+ return subprocess.run(
33
+ cmd,
34
+ cwd=cwd,
35
+ env=env,
36
+ stdout=stdout,
37
+ stderr=stderr,
38
+ shell=isinstance(cmd, str),
39
+ check=check,
40
+ )
41
+
42
+
43
+ async def run_async(
44
+ cmd: Union[Sequence[Union[str, "os.PathLike[str]"]], str],
45
+ *,
46
+ check: bool = True,
47
+ cwd: Optional["os.PathLike[str]"] = None,
48
+ env: Optional[Mapping[str, str]] = None,
49
+ stdout: Optional[Union[int, IO[bytes]]] = None,
50
+ stderr: Optional[Union[int, IO[bytes]]] = None,
51
+ ) -> "CompletedProcess[bytes]":
52
+ if isinstance(cmd, str):
53
+ proc = await asyncio.create_subprocess_shell(
54
+ cmd,
55
+ cwd=cwd,
56
+ env=env,
57
+ stdout=stdout,
58
+ stderr=stderr,
59
+ )
60
+
61
+ else:
62
+ if sys.version_info < (3, 8):
63
+ cmd = [str(arg) for arg in cmd]
64
+ proc = await asyncio.create_subprocess_exec(
65
+ *cmd,
66
+ cwd=cwd,
67
+ env=env,
68
+ stdout=stdout,
69
+ stderr=stderr,
70
+ )
71
+
72
+ output, error = await proc.communicate()
73
+
74
+ assert proc.returncode is not None
75
+
76
+ if check and proc.returncode != 0:
77
+ raise CalledProcessError(
78
+ returncode=proc.returncode,
79
+ cmd=cmd,
80
+ output=output,
81
+ stderr=error,
82
+ )
83
+
84
+ return CompletedProcess(
85
+ cmd, returncode=proc.returncode, stdout=output, stderr=error
86
+ )
foamlib/_cases/_sync.py CHANGED
@@ -1,10 +1,13 @@
1
+ import os
1
2
  import shutil
2
- import subprocess
3
3
  import sys
4
+ import tempfile
4
5
  from pathlib import Path
6
+ from types import TracebackType
5
7
  from typing import (
6
8
  Callable,
7
9
  Optional,
10
+ Type,
8
11
  Union,
9
12
  )
10
13
 
@@ -13,12 +16,16 @@ if sys.version_info >= (3, 9):
13
16
  else:
14
17
  from typing import Collection, Sequence
15
18
 
16
- from .._util import is_sequence
17
- from ._base import FoamCaseBase
18
- from ._util import check_returncode
19
+ if sys.version_info >= (3, 11):
20
+ from typing import Self
21
+ else:
22
+ from typing_extensions import Self
23
+
24
+ from ._recipes import _FoamCaseRecipes
25
+ from ._subprocess import run_sync
19
26
 
20
27
 
21
- class FoamCase(FoamCaseBase):
28
+ class FoamCase(_FoamCaseRecipes):
22
29
  """
23
30
  An OpenFOAM case.
24
31
 
@@ -30,21 +37,34 @@ class FoamCase(FoamCaseBase):
30
37
  """
31
38
 
32
39
  @staticmethod
33
- def _rmtree(path: Path, *, ignore_errors: bool = False) -> None:
40
+ def _rmtree(
41
+ path: Union["os.PathLike[str]", str], *, ignore_errors: bool = False
42
+ ) -> None:
34
43
  shutil.rmtree(path, ignore_errors=ignore_errors)
35
44
 
36
45
  @staticmethod
37
46
  def _copytree(
38
- src: Path,
39
- dest: Path,
47
+ src: Union["os.PathLike[str]", str],
48
+ dest: Union["os.PathLike[str]", str],
40
49
  *,
41
50
  symlinks: bool = False,
42
51
  ignore: Optional[
43
- Callable[[Union[Path, str], Collection[str]], Collection[str]]
52
+ Callable[[Union["os.PathLike[str]", str], Collection[str]], Collection[str]]
44
53
  ] = None,
45
54
  ) -> None:
46
55
  shutil.copytree(src, dest, symlinks=symlinks, ignore=ignore)
47
56
 
57
+ def __enter__(self) -> "FoamCase":
58
+ return self
59
+
60
+ def __exit__(
61
+ self,
62
+ exc_type: Optional[Type[BaseException]],
63
+ exc_val: Optional[BaseException],
64
+ exc_tb: Optional[TracebackType],
65
+ ) -> None:
66
+ self._rmtree(self.path)
67
+
48
68
  def clean(
49
69
  self,
50
70
  *,
@@ -62,43 +82,39 @@ class FoamCase(FoamCaseBase):
62
82
 
63
83
  def _run(
64
84
  self,
65
- cmd: Union[Sequence[Union[str, Path]], str, Path],
85
+ cmd: Union[Sequence[Union[str, "os.PathLike[str]"]], str],
66
86
  *,
67
87
  parallel: bool = False,
68
88
  cpus: int = 1,
69
89
  check: bool = True,
90
+ log: bool = True,
70
91
  ) -> None:
71
- shell = not is_sequence(cmd)
72
-
73
- if parallel:
74
- if shell:
75
- cmd = f"mpiexec -np {cpus} {cmd} -parallel"
76
- else:
77
- assert is_sequence(cmd)
78
- cmd = ["mpiexec", "-np", str(cpus), *cmd, "-parallel"]
79
-
80
- if sys.version_info < (3, 8):
81
- if shell:
82
- cmd = str(cmd)
83
- else:
84
- cmd = (str(arg) for arg in cmd)
85
-
86
- proc = subprocess.run(
87
- cmd,
88
- cwd=self.path,
89
- env=self._env(shell=shell),
90
- stdout=subprocess.DEVNULL,
91
- stderr=subprocess.PIPE if check else subprocess.DEVNULL,
92
- text=True,
93
- shell=shell,
94
- )
95
-
96
- if check:
97
- check_returncode(proc.returncode, cmd, proc.stderr)
92
+ with self._output(cmd, log=log) as (stdout, stderr):
93
+ if parallel:
94
+ if isinstance(cmd, str):
95
+ cmd = [
96
+ "mpiexec",
97
+ "-n",
98
+ str(cpus),
99
+ "/bin/sh",
100
+ "-c",
101
+ f"{cmd} -parallel",
102
+ ]
103
+ else:
104
+ cmd = ["mpiexec", "-n", str(cpus), *cmd, "-parallel"]
105
+
106
+ run_sync(
107
+ cmd,
108
+ check=check,
109
+ cwd=self.path,
110
+ env=self._env(shell=isinstance(cmd, str)),
111
+ stdout=stdout,
112
+ stderr=stderr,
113
+ )
98
114
 
99
115
  def run(
100
116
  self,
101
- cmd: Optional[Union[Sequence[Union[str, Path]], str, Path]] = None,
117
+ cmd: Optional[Union[Sequence[Union[str, "os.PathLike[str]"]], str]] = None,
102
118
  *,
103
119
  script: bool = True,
104
120
  parallel: Optional[bool] = None,
@@ -134,24 +150,34 @@ class FoamCase(FoamCaseBase):
134
150
  for name, args, kwargs in self._restore_0_dir_cmds():
135
151
  getattr(self, name)(*args, **kwargs)
136
152
 
137
- def copy(self, dest: Union[Path, str]) -> "FoamCase":
153
+ def copy(self, dst: Optional[Union["os.PathLike[str]", str]] = None) -> "Self":
138
154
  """
139
155
  Make a copy of this case.
140
156
 
141
- :param dest: The destination path.
157
+ Use as a context manager to automatically delete the copy when done.
158
+
159
+ :param dst: The destination path. If None, copy to a temporary directory.
142
160
  """
143
- for name, args, kwargs in self._copy_cmds(dest):
161
+ if dst is None:
162
+ dst = Path(tempfile.mkdtemp(), self.name)
163
+
164
+ for name, args, kwargs in self._copy_cmds(dst):
144
165
  getattr(self, name)(*args, **kwargs)
145
166
 
146
- return FoamCase(dest)
167
+ return type(self)(dst)
147
168
 
148
- def clone(self, dest: Union[Path, str]) -> "FoamCase":
169
+ def clone(self, dst: Optional[Union["os.PathLike[str]", str]] = None) -> "Self":
149
170
  """
150
171
  Clone this case (make a clean copy).
151
172
 
152
- :param dest: The destination path.
173
+ Use as a context manager to automatically delete the clone when done.
174
+
175
+ :param dst: The destination path. If None, clone to a temporary directory.
153
176
  """
154
- for name, args, kwargs in self._clone_cmds(dest):
177
+ if dst is None:
178
+ dst = Path(tempfile.mkdtemp(), self.name)
179
+
180
+ for name, args, kwargs in self._clone_cmds(dst):
155
181
  getattr(self, name)(*args, **kwargs)
156
182
 
157
- return FoamCase(dest)
183
+ return type(self)(dst)
foamlib/_cases/_util.py CHANGED
@@ -1,35 +1,27 @@
1
- import subprocess
2
- import sys
3
- from pathlib import Path
4
- from typing import Optional, Union
5
- from warnings import warn
1
+ from types import TracebackType
2
+ from typing import Any, AsyncContextManager, Callable, Optional, Type
6
3
 
7
- if sys.version_info >= (3, 9):
8
- from collections.abc import Sequence
9
- else:
10
- from typing import Sequence
11
4
 
5
+ class _AwaitableAsyncContextManager:
6
+ def __init__(self, cm: "AsyncContextManager[Any]"):
7
+ self._cm = cm
12
8
 
13
- class CalledProcessError(subprocess.CalledProcessError):
14
- """Exception raised when a process fails and `check=True`."""
9
+ def __await__(self) -> Any:
10
+ return self._cm.__aenter__().__await__()
15
11
 
16
- def __str__(self) -> str:
17
- msg = super().__str__()
18
- if self.stderr:
19
- msg += f"\n{self.stderr}"
20
- return msg
12
+ async def __aenter__(self) -> Any:
13
+ return await self._cm.__aenter__()
21
14
 
15
+ async def __aexit__(
16
+ self,
17
+ exc_type: Optional[Type[BaseException]],
18
+ exc_val: Optional[BaseException],
19
+ exc_tb: Optional[TracebackType],
20
+ ) -> Any:
21
+ return await self._cm.__aexit__(exc_type, exc_val, exc_tb)
22
22
 
23
- class CalledProcessWarning(Warning):
24
- """Warning raised when a process prints to stderr and `check=True`."""
25
23
 
26
-
27
- def check_returncode(
28
- retcode: int,
29
- cmd: Union[Sequence[Union[str, Path]], str, Path],
30
- stderr: Optional[str],
31
- ) -> None:
32
- if retcode != 0:
33
- raise CalledProcessError(retcode, cmd, None, stderr)
34
- elif stderr:
35
- warn(f"Command {cmd} printed to stderr.\n{stderr}", CalledProcessWarning)
24
+ def awaitableasynccontextmanager(
25
+ cm: Callable[..., "AsyncContextManager[Any]"],
26
+ ) -> Callable[..., _AwaitableAsyncContextManager]:
27
+ return lambda *args, **kwargs: _AwaitableAsyncContextManager(cm(*args, **kwargs))
foamlib/_files/_files.py CHANGED
@@ -16,10 +16,10 @@ if sys.version_info >= (3, 11):
16
16
  else:
17
17
  from typing_extensions import Self
18
18
 
19
- from .._util import is_sequence
20
19
  from ._base import FoamFileBase
21
20
  from ._io import _FoamFileIO
22
21
  from ._serialization import Kind, dumpb
22
+ from ._util import is_sequence
23
23
 
24
24
  try:
25
25
  import numpy as np
@@ -105,13 +105,9 @@ class FoamFile(
105
105
  """
106
106
  Create the file.
107
107
 
108
- Parameters
109
- ----------
110
- exist_ok : bool, optional
111
- If False (the default), raise a FileExistsError if the file already exists.
112
- If True, do nothing if the file already exists.
113
- parents : bool, optional
114
- If True, also create parent directories as needed.
108
+ :param exist_ok: If False, raise a FileExistsError if the file already exists.
109
+
110
+ :param parents: If True, also create parent directories as needed.
115
111
  """
116
112
  if self.path.exists():
117
113
  if not exist_ok:
@@ -8,8 +8,8 @@ if sys.version_info >= (3, 9):
8
8
  else:
9
9
  from typing import Mapping
10
10
 
11
- from .._util import is_sequence
12
11
  from ._base import FoamFileBase
12
+ from ._util import is_sequence
13
13
 
14
14
  try:
15
15
  import numpy as np
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: foamlib
3
- Version: 0.4.4
3
+ Version: 0.5.0
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
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.9
19
19
  Classifier: Programming Language :: Python :: 3.10
20
20
  Classifier: Programming Language :: Python :: 3.11
21
21
  Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
22
23
  Classifier: Topic :: Scientific/Engineering
23
24
  Classifier: Topic :: Software Development
24
25
  Classifier: Typing :: Typed
@@ -123,6 +124,14 @@ my_pitz.clean()
123
124
  my_pitz.control_dict["writeInterval"] = 10
124
125
  ```
125
126
 
127
+ ### Make multiple file reads and writes in a single go
128
+
129
+ ```python
130
+ with my_pitz.fv_schemes as f:
131
+ f["gradSchemes"]["default"] = f["divSchemes"]["default"]
132
+ f["snGradSchemes"]["default"] = "uncorrected"
133
+ ```
134
+
126
135
  ### Run a case asynchronously
127
136
 
128
137
  ```python
@@ -0,0 +1,21 @@
1
+ foamlib/__init__.py,sha256=VR4c4hlAQ8I180EqNsDls6vyYKbnOScayXFOr2fKAEk,392
2
+ foamlib/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ foamlib/_cases/__init__.py,sha256=C0mpRu7c-X-4uVMKmVrZhwIyhBNyvUoCv0o-BQ72RC0,236
4
+ foamlib/_cases/_async.py,sha256=QVZaYJCOaUgt0_xI_cYsZAJcdfpdZoYnh5dojTjMX08,7816
5
+ foamlib/_cases/_base.py,sha256=1CUkkK4afBxDgP79dmho97WJdj-GLgYhnrCSf_52Eao,6604
6
+ foamlib/_cases/_recipes.py,sha256=UTFnVuTvEf-9wn-Fr30a9wOOmyOxvWeDhbqBhdtbhzA,9692
7
+ foamlib/_cases/_subprocess.py,sha256=CfUy_LrqLnMLR9FHINqInCd3soN6eYvonwZ30epiLu8,2234
8
+ foamlib/_cases/_sync.py,sha256=JQAkKXwgdhR_drGy7gadun1XPGjNddKR7Y07HK-TpoA,5964
9
+ foamlib/_cases/_util.py,sha256=4jlv8pe2Ro1-ZtpY5DtR2B1Vpbj84SlqH_HzwGYPoH4,861
10
+ foamlib/_files/__init__.py,sha256=-UqB9YTH6mrJfXCX00kPTAAY20XG64u1MGPw_1ewLVs,148
11
+ foamlib/_files/_base.py,sha256=zaFDjLE6jB7WtGWk8hfKusjLtlGu6CZV16AHJpRUibs,1929
12
+ foamlib/_files/_files.py,sha256=6Fdrc0lksFK99i1a6wEsBi-BtYgSQnUc5s10h2LQ9ew,16091
13
+ foamlib/_files/_io.py,sha256=f_tYI7AqaFsQ8mtK__fEoIUqpYb3YmrI8X5D8updmNM,2084
14
+ foamlib/_files/_parsing.py,sha256=8V2CKZ45mKE3f9fP8lAfexIdhPGrq7elIZkpBkkGB6Q,8773
15
+ foamlib/_files/_serialization.py,sha256=pb8_cIVgRhGS_ZV2p3x8p5_lK1SS6xzQHscAYYuOgFY,3407
16
+ foamlib/_files/_util.py,sha256=UMzXmTFgvbp46w6k3oEZJoYC98pFgEK6LN5uLOwrlCg,397
17
+ foamlib-0.5.0.dist-info/LICENSE.txt,sha256=5Dte9TUnLZzPRs4NQzl-Jc2-Ljd-t_v0ZR5Ng5r0UsY,35131
18
+ foamlib-0.5.0.dist-info/METADATA,sha256=pecBt-ku69uh-HjSN02M40tCqhT22L-sZ9Fcf6AezwM,5759
19
+ foamlib-0.5.0.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
20
+ foamlib-0.5.0.dist-info/top_level.txt,sha256=ZdVYtetXGwPwyfL-WhlhbTFQGAwKX5P_gXxtH9JYFPI,8
21
+ foamlib-0.5.0.dist-info/RECORD,,
@@ -1,19 +0,0 @@
1
- foamlib/__init__.py,sha256=r8VZ1cNvef7NfWWxf7GzPuveJO3LrXVwrF5pwgok-eg,446
2
- foamlib/_util.py,sha256=UMzXmTFgvbp46w6k3oEZJoYC98pFgEK6LN5uLOwrlCg,397
3
- foamlib/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- foamlib/_cases/__init__.py,sha256=xnQpR64EvFtCh07qnSz7kjQ1IhJXjRDwaZWwQGZhGv4,280
5
- foamlib/_cases/_async.py,sha256=WgEunau8GvkI-k4amS98Uxz8O0fZBxLWqzUY3uiAmQQ,6345
6
- foamlib/_cases/_base.py,sha256=4sndDYfXPWhkSel41e8d00xX3PX22wMrYniA_zb0luo,14958
7
- foamlib/_cases/_sync.py,sha256=S-9-TOK7PTC3NMKgIdvKWTn4fS34FPccedvm-lpjpH8,5037
8
- foamlib/_cases/_util.py,sha256=v6sHxHCEgagsVuup0S1xJW-x9py5xj3bUye8PiFfb3o,925
9
- foamlib/_files/__init__.py,sha256=-UqB9YTH6mrJfXCX00kPTAAY20XG64u1MGPw_1ewLVs,148
10
- foamlib/_files/_base.py,sha256=zaFDjLE6jB7WtGWk8hfKusjLtlGu6CZV16AHJpRUibs,1929
11
- foamlib/_files/_files.py,sha256=-3mDRIsaQaxHF74q0Zfzlrhfieb7w_EPOFScSx8wRPE,16245
12
- foamlib/_files/_io.py,sha256=f_tYI7AqaFsQ8mtK__fEoIUqpYb3YmrI8X5D8updmNM,2084
13
- foamlib/_files/_parsing.py,sha256=8V2CKZ45mKE3f9fP8lAfexIdhPGrq7elIZkpBkkGB6Q,8773
14
- foamlib/_files/_serialization.py,sha256=3yb9fgjCpDoRfZoLsbZaIFrkZ3vGBzleFRw6IbaZuuY,3408
15
- foamlib-0.4.4.dist-info/LICENSE.txt,sha256=5Dte9TUnLZzPRs4NQzl-Jc2-Ljd-t_v0ZR5Ng5r0UsY,35131
16
- foamlib-0.4.4.dist-info/METADATA,sha256=i9qq7ZU7oFhmn1BO0rz-53wueFfOvEziP4am3x3qBA4,5496
17
- foamlib-0.4.4.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
18
- foamlib-0.4.4.dist-info/top_level.txt,sha256=ZdVYtetXGwPwyfL-WhlhbTFQGAwKX5P_gXxtH9JYFPI,8
19
- foamlib-0.4.4.dist-info/RECORD,,
File without changes