cht_utils 2.0.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.
@@ -0,0 +1,344 @@
1
+ """Common file and directory operations using pathlib and shutil."""
2
+
3
+ import shutil
4
+ from pathlib import Path
5
+ from typing import List, Union
6
+
7
+ PathLike = Union[str, Path]
8
+
9
+
10
+ def move(src: PathLike, dst: PathLike) -> None:
11
+ """Move files to a destination.
12
+
13
+ If *src* contains glob wildcards, all matching files are moved into *dst*
14
+ (which must be a directory). Without wildcards, *dst* may be a directory
15
+ or a new file name (rename).
16
+
17
+ Parameters
18
+ ----------
19
+ src : str or Path
20
+ Source glob pattern (e.g. ``"data/*.nc"``) or single file/directory.
21
+ dst : str or Path
22
+ Destination directory or new name.
23
+ """
24
+ src = Path(src)
25
+ dst = Path(dst)
26
+ if _has_glob(src):
27
+ for f in src.parent.glob(src.name):
28
+ target = dst / f.name
29
+ if target.exists():
30
+ if target.is_dir():
31
+ shutil.rmtree(target)
32
+ else:
33
+ target.unlink()
34
+ shutil.move(str(f), str(dst))
35
+ else:
36
+ if src.exists():
37
+ # If dst is a directory, the final target is dst/src.name
38
+ target = dst / src.name if dst.is_dir() else dst
39
+ if target.exists():
40
+ if target.is_dir():
41
+ shutil.rmtree(target)
42
+ else:
43
+ target.unlink()
44
+ shutil.move(str(src), str(dst))
45
+
46
+
47
+ def copy(src: PathLike, dst: PathLike) -> None:
48
+ """Copy files or directories to a destination.
49
+
50
+ If *src* contains glob wildcards, all matching files are copied into *dst*
51
+ (which must be a directory). Without wildcards, *dst* may be a directory
52
+ or a new file name.
53
+
54
+ Parameters
55
+ ----------
56
+ src : str or Path
57
+ Source glob pattern or single file/directory.
58
+ dst : str or Path
59
+ Destination directory or new name.
60
+ """
61
+ src = Path(src)
62
+ dst = Path(dst)
63
+ if _has_glob(src):
64
+ for f in src.parent.glob(src.name):
65
+ _copy_single(f, dst / f.name)
66
+ else:
67
+ if not src.exists():
68
+ print(f"{src} does not exist")
69
+ return
70
+ if dst.is_dir():
71
+ _copy_single(src, dst / src.name)
72
+ else:
73
+ _copy_single(src, dst)
74
+
75
+
76
+ def delete(src: Union[PathLike, List[PathLike]]) -> None:
77
+ """Delete files or directories matching glob patterns.
78
+
79
+ Handles both files (``unlink``) and directories (``rmtree``).
80
+ Silently skips paths that don't exist.
81
+
82
+ Parameters
83
+ ----------
84
+ src : str, Path, or list thereof
85
+ Glob pattern(s) or explicit path(s) to delete.
86
+ """
87
+ patterns = src if isinstance(src, list) else [src]
88
+ for pattern in patterns:
89
+ p = Path(pattern)
90
+ if _has_glob(p):
91
+ matches = list(p.parent.glob(p.name))
92
+ else:
93
+ matches = [p] if p.exists() else []
94
+ for f in matches:
95
+ try:
96
+ if f.is_dir():
97
+ shutil.rmtree(f)
98
+ else:
99
+ f.unlink()
100
+ except OSError:
101
+ print(f"Could not delete {f}")
102
+
103
+
104
+ def rename(src: PathLike, dst: PathLike) -> Path:
105
+ """Rename a file or directory.
106
+
107
+ Works across drives on Windows (falls back to copy + delete).
108
+
109
+ Parameters
110
+ ----------
111
+ src : str or Path
112
+ Source path.
113
+ dst : str or Path
114
+ New name or path.
115
+
116
+ Returns
117
+ -------
118
+ Path
119
+ The new path.
120
+ """
121
+ src = Path(src)
122
+ dst = Path(dst)
123
+ try:
124
+ return src.rename(dst)
125
+ except OSError:
126
+ # Cross-drive rename on Windows
127
+ if src.is_dir():
128
+ shutil.copytree(src, dst)
129
+ shutil.rmtree(src)
130
+ else:
131
+ shutil.copy2(src, dst)
132
+ src.unlink()
133
+ return dst
134
+
135
+
136
+ def mkdir(path: PathLike) -> Path:
137
+ """Create a directory (including parents) if it does not exist.
138
+
139
+ Parameters
140
+ ----------
141
+ path : str or Path
142
+ Directory path to create.
143
+
144
+ Returns
145
+ -------
146
+ Path
147
+ The created directory path.
148
+ """
149
+ p = Path(path)
150
+ p.mkdir(parents=True, exist_ok=True)
151
+ return p
152
+
153
+
154
+ def touch(path: PathLike) -> Path:
155
+ """Create an empty file or update its modification timestamp.
156
+
157
+ Parent directories are created if needed.
158
+
159
+ Parameters
160
+ ----------
161
+ path : str or Path
162
+ File path.
163
+
164
+ Returns
165
+ -------
166
+ Path
167
+ The touched file path.
168
+ """
169
+ p = Path(path)
170
+ p.parent.mkdir(parents=True, exist_ok=True)
171
+ p.touch()
172
+ return p
173
+
174
+
175
+ def file_size(path: PathLike) -> int:
176
+ """Return the size of a file in bytes.
177
+
178
+ Parameters
179
+ ----------
180
+ path : str or Path
181
+ File path.
182
+
183
+ Returns
184
+ -------
185
+ int
186
+ File size in bytes.
187
+
188
+ Raises
189
+ ------
190
+ FileNotFoundError
191
+ If the file does not exist.
192
+ """
193
+ return Path(path).stat().st_size
194
+
195
+
196
+ def list_files(
197
+ src: PathLike,
198
+ pattern: str = "*",
199
+ recursive: bool = False,
200
+ full_path: bool = True,
201
+ ) -> List[str]:
202
+ """List files in a directory, optionally filtered by glob pattern.
203
+
204
+ Parameters
205
+ ----------
206
+ src : str or Path
207
+ Directory path, or glob pattern for backward compatibility
208
+ (e.g. ``"data/*.nc"``).
209
+ pattern : str
210
+ Glob pattern to filter files. Defaults to ``"*"`` (all files).
211
+ Ignored when *src* itself contains wildcards.
212
+ recursive : bool
213
+ If ``True``, search subdirectories recursively.
214
+ full_path : bool
215
+ If ``True``, return full paths. If ``False``, return basenames.
216
+
217
+ Returns
218
+ -------
219
+ List[str]
220
+ Sorted list of file paths or names.
221
+ """
222
+
223
+ # Collapse consecutive wildcards — pathlib rejects "**" mixed with text
224
+ while "**" in pattern:
225
+ pattern = pattern.replace("**", "*")
226
+ p = Path(src)
227
+ if _has_glob(p):
228
+ # Legacy: src is a glob pattern like "data/*.nc"
229
+ name = p.name
230
+ while "**" in name:
231
+ name = name.replace("**", "*")
232
+ files = list(p.parent.glob(name))
233
+ elif recursive:
234
+ files = list(p.rglob(pattern))
235
+ else:
236
+ files = list(p.glob(pattern))
237
+ files = [f for f in files if f.is_file()]
238
+ if full_path:
239
+ return sorted(str(f) for f in files)
240
+ return sorted(f.name for f in files)
241
+
242
+
243
+ def list_folders(
244
+ src: PathLike, pattern: str = "*", basename: bool = False
245
+ ) -> List[str]:
246
+ """List subdirectories, optionally filtered by glob pattern.
247
+
248
+ Parameters
249
+ ----------
250
+ src : str or Path
251
+ Directory path, or glob pattern for backward compatibility.
252
+ pattern : str
253
+ Glob pattern to filter folders. Defaults to ``"*"``.
254
+ Ignored when *src* itself contains wildcards.
255
+ basename : bool
256
+ If ``True``, return only folder names instead of full paths.
257
+
258
+ Returns
259
+ -------
260
+ List[str]
261
+ Sorted list of folder paths or names.
262
+ """
263
+ p = Path(src)
264
+ if _has_glob(p):
265
+ folders = list(p.parent.glob(p.name))
266
+ else:
267
+ folders = list(p.glob(pattern))
268
+ folders = [f for f in folders if f.is_dir()]
269
+ if basename:
270
+ return sorted(f.name for f in folders)
271
+ return sorted(str(f) for f in folders)
272
+
273
+
274
+ def exists(src: PathLike) -> bool:
275
+ """Check if a file or directory exists.
276
+
277
+ Parameters
278
+ ----------
279
+ src : str or Path
280
+ Path to check.
281
+
282
+ Returns
283
+ -------
284
+ bool
285
+ """
286
+ return Path(src).exists()
287
+
288
+
289
+ def find_replace(file_name: PathLike, old: str, new: str) -> None:
290
+ """Replace all occurrences of a string in a file.
291
+
292
+ Parameters
293
+ ----------
294
+ file_name : str or Path
295
+ File to modify in-place.
296
+ old : str
297
+ String to search for.
298
+ new : str
299
+ Replacement string.
300
+ """
301
+ p = Path(file_name)
302
+ p.write_text(p.read_text().replace(old, new))
303
+
304
+
305
+ # ── Internal helpers ──────────────────────────────────────────────────
306
+
307
+
308
+ def _has_glob(p: Path) -> bool:
309
+ """Check if a path contains glob wildcard characters."""
310
+ return any(c in str(p) for c in ("*", "?", "["))
311
+
312
+
313
+ def _copy_single(src: Path, dst: Path) -> None:
314
+ """Copy a single file or directory to a destination path."""
315
+ if dst.exists():
316
+ if dst.is_dir():
317
+ shutil.rmtree(dst)
318
+ else:
319
+ dst.unlink()
320
+ if src.is_dir():
321
+ shutil.copytree(src, dst)
322
+ else:
323
+ shutil.copy2(src, dst)
324
+
325
+
326
+ # ── Backward-compatible aliases ───────────────────────────────────────
327
+
328
+ move_file = move
329
+ copy_file = copy
330
+ delete_file = delete
331
+ delete_folder = delete
332
+ rm = delete
333
+ rmdir = delete
334
+ findreplace = find_replace
335
+
336
+
337
+ def list_all_files(src: PathLike) -> List[str]:
338
+ """Backward-compatible alias for ``list_files(src, recursive=True)``."""
339
+ return list_files(src, recursive=True)
340
+
341
+
342
+ def list_files_recursive(src: PathLike, pattern: str = "*") -> List[str]:
343
+ """Backward-compatible alias for ``list_files(src, pattern, recursive=True)``."""
344
+ return list_files(src, pattern=pattern, recursive=True)
@@ -0,0 +1,5 @@
1
+ """Spatial interpolation utilities for regular and unstructured grids."""
2
+
3
+ from cht_utils.interpolation.interpolation import interp2 as interp2
4
+ from cht_utils.interpolation.interpolation import interp2_bilinear as interp2_bilinear
5
+ from cht_utils.interpolation.interpolation import interp3 as interp3
@@ -0,0 +1,152 @@
1
+ """Spatial interpolation utilities for regular and unstructured grids."""
2
+
3
+ import numpy as np
4
+ from scipy.interpolate import RegularGridInterpolator, griddata
5
+
6
+
7
+ def interp2(
8
+ x0: np.ndarray,
9
+ y0: np.ndarray,
10
+ z0: np.ndarray,
11
+ x1: np.ndarray,
12
+ y1: np.ndarray,
13
+ method: str = "linear",
14
+ ) -> np.ndarray:
15
+ """Interpolate from a regular grid to arbitrary points.
16
+
17
+ Parameters
18
+ ----------
19
+ x0 : np.ndarray
20
+ 1-D array of x-coordinates of the source grid.
21
+ y0 : np.ndarray
22
+ 1-D array of y-coordinates of the source grid.
23
+ z0 : np.ndarray
24
+ 2-D array of values on the source grid, shape ``(len(y0), len(x0))``.
25
+ x1 : np.ndarray
26
+ Target x-coordinates (1-D or 2-D).
27
+ y1 : np.ndarray
28
+ Target y-coordinates (1-D or 2-D).
29
+ method : str
30
+ Interpolation method (``"linear"``, ``"nearest"``).
31
+
32
+ Returns
33
+ -------
34
+ np.ndarray
35
+ Interpolated values at the target locations.
36
+ """
37
+ f = RegularGridInterpolator(
38
+ (y0, x0), z0, bounds_error=False, fill_value=np.nan, method=method
39
+ )
40
+ if x1.ndim > 1:
41
+ sz = x1.shape
42
+ z1 = f((y1.ravel(), x1.ravel())).reshape(sz)
43
+ else:
44
+ z1 = f((y1, x1))
45
+ return z1
46
+
47
+
48
+ def interp2_bilinear(
49
+ xp: np.ndarray,
50
+ yp: np.ndarray,
51
+ zp: np.ndarray,
52
+ x: np.ndarray,
53
+ y: np.ndarray,
54
+ ) -> np.ndarray:
55
+ """Bilinear interpolation on a regular grid.
56
+
57
+ Parameters
58
+ ----------
59
+ xp : np.ndarray
60
+ 1-D array of source x-coordinates (monotonically increasing).
61
+ yp : np.ndarray
62
+ 1-D array of source y-coordinates (monotonically increasing).
63
+ zp : np.ndarray
64
+ 2-D source values, shape ``(len(yp), len(xp))``.
65
+ x : np.ndarray
66
+ Target x-coordinates.
67
+ y : np.ndarray
68
+ Target y-coordinates.
69
+
70
+ Returns
71
+ -------
72
+ np.ndarray
73
+ Interpolated values at the target locations.
74
+ """
75
+ dx = xp[1] - xp[0]
76
+ dy = yp[1] - yp[0]
77
+
78
+ col = (x - xp[0]) / dx
79
+ row = (y - yp[0]) / dy
80
+
81
+ c0 = np.floor(col).astype(int)
82
+ r0 = np.floor(row).astype(int)
83
+ c1 = c0 + 1
84
+ r1 = r0 + 1
85
+
86
+ # Clip to valid range
87
+ c0 = np.clip(c0, 0, len(xp) - 1)
88
+ c1 = np.clip(c1, 0, len(xp) - 1)
89
+ r0 = np.clip(r0, 0, len(yp) - 1)
90
+ r1 = np.clip(r1, 0, len(yp) - 1)
91
+
92
+ # Fractional parts
93
+ dc = col - np.floor(col)
94
+ dr = row - np.floor(row)
95
+
96
+ z = (
97
+ zp[r0, c0] * (1 - dc) * (1 - dr)
98
+ + zp[r0, c1] * dc * (1 - dr)
99
+ + zp[r1, c0] * (1 - dc) * dr
100
+ + zp[r1, c1] * dc * dr
101
+ )
102
+ return z
103
+
104
+
105
+ def interp3(
106
+ x0: np.ndarray,
107
+ y0: np.ndarray,
108
+ z0: np.ndarray,
109
+ x1: np.ndarray,
110
+ y1: np.ndarray,
111
+ method: str = "linear",
112
+ ) -> np.ndarray:
113
+ """Interpolate from unstructured or curvilinear points.
114
+
115
+ Uses :func:`scipy.interpolate.griddata` for scattered-data interpolation.
116
+
117
+ Parameters
118
+ ----------
119
+ x0 : np.ndarray
120
+ Source x-coordinates (1-D or 2-D).
121
+ y0 : np.ndarray
122
+ Source y-coordinates (1-D or 2-D).
123
+ z0 : np.ndarray
124
+ Source values.
125
+ x1 : np.ndarray
126
+ Target x-coordinates (1-D or 2-D).
127
+ y1 : np.ndarray
128
+ Target y-coordinates (1-D or 2-D).
129
+ method : str
130
+ Interpolation method (``"linear"``, ``"nearest"``, ``"cubic"``).
131
+
132
+ Returns
133
+ -------
134
+ np.ndarray
135
+ Interpolated values at the target locations.
136
+ """
137
+ if x1.ndim > 1:
138
+ sz = x1.shape
139
+ z1 = griddata(
140
+ (x0.ravel(), y0.ravel()),
141
+ z0.ravel(),
142
+ (x1.ravel(), y1.ravel()),
143
+ method=method,
144
+ ).reshape(sz)
145
+ else:
146
+ z1 = griddata(
147
+ (x0.ravel(), y0.ravel()),
148
+ z0.ravel(),
149
+ (x1, y1),
150
+ method=method,
151
+ )
152
+ return z1
@@ -0,0 +1,2 @@
1
+ from cht_utils.maps.flood_map import *
2
+ from cht_utils.maps.topobathy_map import *
@@ -0,0 +1,191 @@
1
+ """File and directory operations for copying, moving, deleting, and listing paths."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import glob
6
+ import logging
7
+ import os
8
+ import shutil
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def move_file(src: str, dst: str) -> None:
14
+ """Move files matching a glob pattern to a destination directory.
15
+
16
+ Parameters
17
+ ----------
18
+ src : str
19
+ Glob pattern for source files.
20
+ dst : str
21
+ Destination directory path.
22
+ """
23
+ for full_file_name in glob.glob(src):
24
+ src_name = os.path.basename(full_file_name)
25
+ if os.path.exists(os.path.join(dst, src_name)):
26
+ try:
27
+ os.remove(os.path.join(dst, src_name))
28
+ except Exception:
29
+ logger.error(f"Could not remove file {os.path.join(dst, src_name)}")
30
+ try:
31
+ shutil.move(full_file_name, dst)
32
+ except Exception:
33
+ logger.error(f"Could not move file {full_file_name}")
34
+
35
+
36
+ def copy_file(src: str, dst: str) -> None:
37
+ """Copy files matching a glob pattern to a destination directory.
38
+
39
+ Parameters
40
+ ----------
41
+ src : str
42
+ Glob pattern for source files.
43
+ dst : str
44
+ Destination directory path.
45
+ """
46
+ for full_file_name in glob.glob(src):
47
+ src_name = os.path.basename(full_file_name)
48
+ if os.path.exists(os.path.join(dst, src_name)):
49
+ os.remove(os.path.join(dst, src_name))
50
+ if os.path.isdir(full_file_name):
51
+ dstf = os.path.join(dst, os.path.basename(full_file_name))
52
+ shutil.copytree(full_file_name, dstf)
53
+ else:
54
+ shutil.copy(full_file_name, dst)
55
+
56
+
57
+ def delete_file(src: str) -> None:
58
+ """Delete files matching a glob pattern.
59
+
60
+ Parameters
61
+ ----------
62
+ src : str
63
+ Glob pattern for files to delete.
64
+ """
65
+ for file_name in glob.glob(src):
66
+ try:
67
+ os.remove(src)
68
+ except Exception:
69
+ logger.error(f"Could not delete {src}")
70
+
71
+
72
+ def rm(src: str) -> None:
73
+ """Remove a single file.
74
+
75
+ Parameters
76
+ ----------
77
+ src : str
78
+ Path to the file to remove.
79
+ """
80
+ os.remove(src)
81
+
82
+
83
+ def mkdir(path: str) -> None:
84
+ """Create a directory (and parents) if it does not already exist.
85
+
86
+ Parameters
87
+ ----------
88
+ path : str
89
+ Directory path to create.
90
+ """
91
+ if not os.path.exists(path):
92
+ os.makedirs(path)
93
+
94
+
95
+ def list_files(src: str, full_path: bool = True) -> list[str]:
96
+ """List files matching a glob pattern or directory listing.
97
+
98
+ Parameters
99
+ ----------
100
+ src : str
101
+ Glob pattern (when *full_path* is True) or directory path (when False).
102
+ full_path : bool
103
+ If True, use glob and return full paths. If False, use ``os.listdir``.
104
+
105
+ Returns
106
+ -------
107
+ list[str]
108
+ Sorted list of file paths or names.
109
+ """
110
+ if full_path:
111
+ file_list = []
112
+ full_list = glob.glob(src)
113
+ for item in full_list:
114
+ if os.path.isfile(item):
115
+ file_list.append(item)
116
+ else:
117
+ file_list = os.listdir(src)
118
+
119
+ return sorted(file_list)
120
+
121
+
122
+ def list_folders(src: str, basename: bool = False) -> list[str]:
123
+ """List directories matching a glob pattern.
124
+
125
+ Parameters
126
+ ----------
127
+ src : str
128
+ Glob pattern.
129
+ basename : bool
130
+ If True, return only base names instead of full paths.
131
+
132
+ Returns
133
+ -------
134
+ list[str]
135
+ Sorted list of directory paths (or base names).
136
+ """
137
+ folder_list = []
138
+ full_list = glob.glob(src)
139
+ for item in full_list:
140
+ if os.path.isdir(item):
141
+ if basename:
142
+ folder_list.append(os.path.basename(item))
143
+ else:
144
+ folder_list.append(item)
145
+
146
+ return sorted(folder_list)
147
+
148
+
149
+ def delete_folder(src: str) -> None:
150
+ """Recursively delete a directory tree.
151
+
152
+ Parameters
153
+ ----------
154
+ src : str
155
+ Path to the directory to delete.
156
+ """
157
+ try:
158
+ shutil.rmtree(src, ignore_errors=False, onerror=None)
159
+ except Exception:
160
+ logger.error(f"Could not delete folder {src}")
161
+
162
+
163
+ def rmdir(src: str) -> None:
164
+ """Recursively delete a directory tree if it exists.
165
+
166
+ Parameters
167
+ ----------
168
+ src : str
169
+ Path to the directory to delete.
170
+ """
171
+ try:
172
+ if os.path.exists(src):
173
+ shutil.rmtree(src, ignore_errors=False, onerror=None)
174
+ except Exception:
175
+ logger.error(f"Could not delete folder {src}")
176
+
177
+
178
+ def exists(src: str) -> bool:
179
+ """Check whether a path exists.
180
+
181
+ Parameters
182
+ ----------
183
+ src : str
184
+ Path to check.
185
+
186
+ Returns
187
+ -------
188
+ bool
189
+ True if the path exists.
190
+ """
191
+ return os.path.exists(src)