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,234 @@
1
+ """Read and write Delft3D Tekal ASCII files.
2
+
3
+ A Tekal file consists of consecutive blocks, each containing:
4
+
5
+ - Optional ``*``-prefixed comment lines
6
+ - A block name line
7
+ - A size line (nrows, ncols, and optionally nx, ny)
8
+ - Data lines
9
+
10
+ Example usage::
11
+
12
+ import cht_utils.fileio.tekal as tek
13
+
14
+ D = tek.tekal("myfile.tek")
15
+ D.info()
16
+ print(D)
17
+ M = D.read(0) # load first block
18
+ """
19
+
20
+ from typing import IO, List, Optional
21
+
22
+ import numpy as np
23
+
24
+
25
+ class tekalblock:
26
+ """A single data block within a Tekal file."""
27
+
28
+ def __init__(self) -> None:
29
+ self.header: List[str] = []
30
+ self.nhdr: Optional[int] = None
31
+ self.index: Optional[int] = None
32
+ self.name: Optional[str] = None
33
+ self.npts: Optional[int] = None
34
+ self.nvar: Optional[int] = None
35
+ self.nx: Optional[int] = None
36
+ self.ny: Optional[int] = None
37
+ self.data: Optional[np.ndarray] = None
38
+ self.tell: float = 0.0
39
+
40
+ def __str__(self) -> str:
41
+ return (
42
+ f"{self.index:5g}: nvar={self.nvar:4g} npts={self.npts:5g} "
43
+ f"({self.nx:5g} x{self.ny:4g}) {self.nhdr:4} '{self.name}'\n"
44
+ )
45
+
46
+ def strheader(self) -> str:
47
+ """Return a header line describing the column layout."""
48
+ return (
49
+ f"{'block':>5s}: {'nvar':>4s} {'npts':>5s} "
50
+ f"({'nx':>5s} x{'ny':>4s}) {'nhdr':>4s} '{'blockname'}'\n"
51
+ )
52
+
53
+ def info(self, fid: IO, index: int) -> Optional["tekalblock"]:
54
+ """Read block metadata (header, name, dimensions) from an open file.
55
+
56
+ Parameters
57
+ ----------
58
+ fid : IO
59
+ Open file handle.
60
+ index : int
61
+ Block index.
62
+
63
+ Returns
64
+ -------
65
+ tekalblock or None
66
+ Self if successful, ``None`` at end-of-file.
67
+ """
68
+ self.tell = fid.tell()
69
+
70
+ rec = fid.readline()
71
+ if len(rec) == 0:
72
+ return None
73
+
74
+ while rec[0] == "*":
75
+ self.header.append(rec)
76
+ rec = fid.readline()
77
+
78
+ self.index = index
79
+ self.name = rec.split()[0]
80
+ self.header.append(rec)
81
+ self.nhdr = len(self.header)
82
+
83
+ rec = fid.readline()
84
+ parts = rec.split()
85
+ self.npts = int(parts[0])
86
+ self.nvar = int(parts[1])
87
+ if len(parts) == 2:
88
+ self.nx = self.npts
89
+ self.ny = 1
90
+ else:
91
+ self.nx = int(parts[2])
92
+ self.ny = int(parts[3])
93
+
94
+ # Skip data lines
95
+ for _ in range(self.npts):
96
+ fid.readline()
97
+
98
+ return self
99
+
100
+ def load(self, fid: IO) -> np.ndarray:
101
+ """Load the data from this block into a 3-D numpy array.
102
+
103
+ The returned array has shape ``(nvar, nx, ny)``.
104
+
105
+ Parameters
106
+ ----------
107
+ fid : IO
108
+ Open file handle, sought to ``self.tell``.
109
+
110
+ Returns
111
+ -------
112
+ np.ndarray
113
+ Data array of shape ``(nvar, nx, ny)``.
114
+ """
115
+ # Skip header
116
+ rec = fid.readline()
117
+ while rec[0] == "*":
118
+ rec = fid.readline()
119
+
120
+ rec = fid.readline()
121
+ parts = rec.split()
122
+ npts = int(parts[0])
123
+ nvar = int(parts[1])
124
+ if len(parts) == 2:
125
+ nx, ny = npts, 1
126
+ else:
127
+ nx, ny = int(parts[2]), int(parts[3])
128
+
129
+ M = np.zeros([nvar, nx, ny])
130
+ for ix in range(nx):
131
+ for it in range(ny):
132
+ rec = fid.readline()
133
+ values = rec.split()
134
+ M[:, ix, it] = [float(v) for v in values[:nvar]]
135
+
136
+ self.data = M
137
+ return M
138
+
139
+ def check(self) -> None:
140
+ """Assert that all required fields are populated."""
141
+ assert self.header != []
142
+ assert self.nhdr is not None
143
+ assert self.name is not None
144
+ assert self.npts is not None
145
+ assert self.nvar is not None
146
+ assert self.nx is not None
147
+ assert self.ny is not None
148
+ assert self.data is not None
149
+
150
+ def copy(self) -> "tekalblock":
151
+ """Create a deep copy of this block."""
152
+ cp = tekalblock()
153
+ cp.header = self.header[:]
154
+ cp.nhdr = self.nhdr
155
+ cp.name = self.name
156
+ cp.npts = self.npts
157
+ cp.nvar = self.nvar
158
+ cp.nx = self.nx
159
+ cp.ny = self.ny
160
+ cp.data = self.data.copy()
161
+ return cp
162
+
163
+
164
+ class tekal:
165
+ """Container for a Tekal file with multiple blocks.
166
+
167
+ Parameters
168
+ ----------
169
+ filename : str
170
+ Path to the Tekal file.
171
+ """
172
+
173
+ def __init__(self, filename: str) -> None:
174
+ self.filename = filename
175
+ self.blocks: List[tekalblock] = []
176
+
177
+ def append(self, block: tekalblock) -> None:
178
+ """Append a validated block to the file structure."""
179
+ block.check()
180
+ self.blocks.append(block)
181
+ self.blocks[-1].index = len(self.blocks) - 1
182
+
183
+ def __str__(self) -> str:
184
+ txt = self.blocks[0].strheader()
185
+ for block in self.blocks:
186
+ txt += str(block)
187
+ return txt
188
+
189
+ def info(self) -> None:
190
+ """Read metadata for all blocks (without loading data)."""
191
+ fid = open(self.filename, "rb")
192
+ index = 0
193
+ while True:
194
+ B = tekalblock()
195
+ result = B.info(fid, index)
196
+ if result is None or B.npts is None:
197
+ break
198
+ self.blocks.append(B)
199
+ index += 1
200
+ fid.close()
201
+
202
+ def read(self, index: int) -> np.ndarray:
203
+ """Load data from a specific block.
204
+
205
+ Parameters
206
+ ----------
207
+ index : int
208
+ Block index.
209
+
210
+ Returns
211
+ -------
212
+ np.ndarray
213
+ Data array of shape ``(nvar, nx, ny)``.
214
+ """
215
+ fid = open(self.filename, "rb")
216
+ fid.seek(self.blocks[index].tell)
217
+ M = self.blocks[index].load(fid)
218
+ fid.close()
219
+ return M
220
+
221
+ def write(self) -> None:
222
+ """Write all blocks to the file."""
223
+ with open(self.filename, "a") as fid:
224
+ for block in self.blocks:
225
+ block.check()
226
+ fid.write("".join(block.header))
227
+ if block.ny == 1:
228
+ fid.write(f"{block.npts:g} {block.nvar:g}\n")
229
+ else:
230
+ fid.write(f"{block.npts:g} {block.nvar:g} {block.ny:g}\n")
231
+ for it in range(block.ny):
232
+ for ix in range(block.nx):
233
+ row = " ".join(f"{v:f}" for v in block.data[:, ix, it])
234
+ fid.write(row + "\n")
@@ -0,0 +1,184 @@
1
+ """XML serialization and deserialization with type conversion.
2
+
3
+ Supports reading/writing Python objects and dictionaries as XML, with
4
+ optional type attributes (``float``, ``int``, ``datetime``) for automatic
5
+ value conversion.
6
+ """
7
+
8
+ import urllib.request
9
+ import xml.etree.ElementTree as ET
10
+ from datetime import datetime
11
+ from typing import IO, Any
12
+
13
+
14
+ class XMLObject:
15
+ """Marker base class for XML-deserialized objects."""
16
+
17
+ pass
18
+
19
+
20
+ def write_node(file: IO, node: Any, nspaces: int) -> None:
21
+ """Recursively write a node (dict or object) to an XML file.
22
+
23
+ Parameters
24
+ ----------
25
+ file : IO
26
+ Open file handle.
27
+ node : dict or object
28
+ Node to serialize.
29
+ nspaces : int
30
+ Current indentation level.
31
+ """
32
+ node_dict = node if isinstance(node, dict) else node.__dict__
33
+ spaces = " " * nspaces
34
+
35
+ for key, children in node_dict.items():
36
+ for child in children:
37
+ if isinstance(child, dict) or (
38
+ not isinstance(child, dict) and not hasattr(child, "value")
39
+ ):
40
+ if hasattr(child, "value"):
41
+ _write_leaf(file, spaces, key, child)
42
+ else:
43
+ file.write(f"{spaces}<{key}>\n")
44
+ write_node(file, child, nspaces + 2)
45
+ file.write(f"{spaces}</{key}>\n")
46
+ else:
47
+ _write_leaf(file, spaces, key, child)
48
+
49
+
50
+ def _write_leaf(file: IO, spaces: str, key: str, node: Any) -> None:
51
+ """Write a leaf node with optional type attribute."""
52
+ if isinstance(node.value, int):
53
+ attr = ' type="int"'
54
+ val = str(node.value)
55
+ elif isinstance(node.value, float):
56
+ attr = ' type="float"'
57
+ val = str(node.value)
58
+ else:
59
+ attr = ""
60
+ val = node.value
61
+ file.write(f"{spaces}<{key}{attr}>{val}</{key}>\n")
62
+
63
+
64
+ def obj2xml(obj: Any, file_name: str) -> None:
65
+ """Serialize a Python object to an XML file.
66
+
67
+ Parameters
68
+ ----------
69
+ obj : Any
70
+ Object whose ``__dict__`` will be serialized.
71
+ file_name : str
72
+ Output file path.
73
+ """
74
+ with open(file_name, "w") as f:
75
+ f.write('<?xml version="1.0"?>\n<root>\n')
76
+ write_node(f, obj, 2)
77
+ f.write("</root>\n")
78
+
79
+
80
+ def dict2xml(xml_dict: dict, file_name: str) -> None:
81
+ """Serialize a dictionary to an XML file.
82
+
83
+ Parameters
84
+ ----------
85
+ xml_dict : dict
86
+ Dictionary to serialize.
87
+ file_name : str
88
+ Output file path.
89
+ """
90
+ with open(file_name, "w") as f:
91
+ f.write('<?xml version="1.0"?>\n<root>\n')
92
+ write_node(f, xml_dict, 2)
93
+ f.write("</root>\n")
94
+
95
+
96
+ def xml2obj(file_name: str) -> Any:
97
+ """Deserialize an XML file (local or URL) to a Python object.
98
+
99
+ Parameters
100
+ ----------
101
+ file_name : str
102
+ Local file path or HTTP(S) URL.
103
+
104
+ Returns
105
+ -------
106
+ Any
107
+ Dynamically typed Python object tree.
108
+ """
109
+ if file_name.startswith("http"):
110
+ with urllib.request.urlopen(file_name) as f:
111
+ tree = ET.parse(f)
112
+ xml_root = tree.getroot()
113
+ else:
114
+ xml_root = ET.parse(file_name).getroot()
115
+ return _xml2py(xml_root)
116
+
117
+
118
+ def get_value(file_name: str, tag: str) -> Any:
119
+ """Read a single value from an XML file by tag path.
120
+
121
+ Parameters
122
+ ----------
123
+ file_name : str
124
+ XML file path.
125
+ tag : str
126
+ XPath-style tag (e.g. ``"section/param"``).
127
+
128
+ Returns
129
+ -------
130
+ Any
131
+ The value, with type conversion applied if a ``type`` attribute exists.
132
+ """
133
+ xml_root = ET.parse(file_name).getroot()
134
+ node = xml_root.findall(tag)[0]
135
+ val = node.text
136
+ if node.attrib and "type" in node.attrib:
137
+ val = _convert_value(node.text, node.attrib["type"])
138
+ return val
139
+
140
+
141
+ def _convert_value(text: str, type_name: str) -> Any:
142
+ """Convert a string value based on a type name."""
143
+ if type_name == "float":
144
+ return float(text)
145
+ elif type_name == "int":
146
+ return int(text)
147
+ elif type_name == "datetime":
148
+ return datetime.strptime(text, "%Y%m%d %H%M%S")
149
+ return text
150
+
151
+
152
+ def _xml2py(node: ET.Element) -> Any:
153
+ """Recursively convert an XML element to a Python object."""
154
+ name = node.tag
155
+ pytype = type(name, (object,), {})
156
+ pyobj = pytype()
157
+
158
+ for attr in node.attrib:
159
+ setattr(pyobj, attr, node.get(attr))
160
+
161
+ if node.text and node.text.strip() not in ("", "\n"):
162
+ setattr(pyobj, "text", node.text)
163
+ setattr(pyobj, "value", node.text)
164
+ if node.attrib and "type" in node.attrib:
165
+ type_name = node.attrib["type"]
166
+ if type_name == "float":
167
+ lst = node.text.split(",")
168
+ pyobj.value = (
169
+ float(node.text) if len(lst) == 1 else [float(s) for s in lst]
170
+ )
171
+ elif type_name == "int":
172
+ if "," in node.text:
173
+ pyobj.value = [int(s) for s in node.text.split(",")]
174
+ else:
175
+ pyobj.value = int(node.text)
176
+ elif type_name == "datetime":
177
+ pyobj.value = datetime.strptime(node.text, "%Y%m%d %H%M%S")
178
+
179
+ for cn in node:
180
+ if not hasattr(pyobj, cn.tag):
181
+ setattr(pyobj, cn.tag, [])
182
+ getattr(pyobj, cn.tag).append(_xml2py(cn))
183
+
184
+ return pyobj
@@ -0,0 +1,39 @@
1
+ """YAML file read/write utilities."""
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ import yaml
6
+
7
+
8
+ def dict2yaml(file_name: str, dct: Dict[str, Any], sort_keys: bool = False) -> None:
9
+ """Write a dictionary to a YAML file.
10
+
11
+ Parameters
12
+ ----------
13
+ file_name : str
14
+ Output file path.
15
+ dct : Dict[str, Any]
16
+ Dictionary to serialize.
17
+ sort_keys : bool
18
+ Whether to sort keys alphabetically.
19
+ """
20
+ yaml_string = yaml.dump(dct, sort_keys=sort_keys)
21
+ with open(file_name, "w") as f:
22
+ f.write(yaml_string)
23
+
24
+
25
+ def yaml2dict(file_name: str) -> Optional[Dict[str, Any]]:
26
+ """Read a YAML file into a dictionary.
27
+
28
+ Parameters
29
+ ----------
30
+ file_name : str
31
+ Input file path.
32
+
33
+ Returns
34
+ -------
35
+ Dict[str, Any] or None
36
+ Parsed dictionary, or ``None`` for empty files.
37
+ """
38
+ with open(file_name, "r") as f:
39
+ return yaml.load(f, Loader=yaml.FullLoader)
@@ -0,0 +1,25 @@
1
+ """File and directory operations."""
2
+
3
+ # Core API
4
+ from cht_utils.fileops.fileops import copy as copy
5
+
6
+ # Backward-compatible aliases
7
+ from cht_utils.fileops.fileops import copy_file as copy_file
8
+ from cht_utils.fileops.fileops import delete as delete
9
+ from cht_utils.fileops.fileops import delete_file as delete_file
10
+ from cht_utils.fileops.fileops import delete_folder as delete_folder
11
+ from cht_utils.fileops.fileops import exists as exists
12
+ from cht_utils.fileops.fileops import file_size as file_size
13
+ from cht_utils.fileops.fileops import find_replace as find_replace
14
+ from cht_utils.fileops.fileops import findreplace as findreplace
15
+ from cht_utils.fileops.fileops import list_all_files as list_all_files
16
+ from cht_utils.fileops.fileops import list_files as list_files
17
+ from cht_utils.fileops.fileops import list_files_recursive as list_files_recursive
18
+ from cht_utils.fileops.fileops import list_folders as list_folders
19
+ from cht_utils.fileops.fileops import mkdir as mkdir
20
+ from cht_utils.fileops.fileops import move as move
21
+ from cht_utils.fileops.fileops import move_file as move_file
22
+ from cht_utils.fileops.fileops import rename as rename
23
+ from cht_utils.fileops.fileops import rm as rm
24
+ from cht_utils.fileops.fileops import rmdir as rmdir
25
+ from cht_utils.fileops.fileops import touch as touch