tecio-python 0.2.0__py3-none-any.whl → 0.2.1__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.
tecio/cli/tecaux.py ADDED
@@ -0,0 +1,700 @@
1
+ r"""Add dataset-, zone-, or variable-level auxiliary data to a Tecplot data file.
2
+
3
+ Tecplot's auxiliary data mechanism attaches arbitrary ``name=value`` string metadata to
4
+ a dataset, a zone, or a variable -- solver name, run date, units, a description, or any
5
+ other annotation that doesn't belong in the numerical data itself. Adding this after
6
+ the fact ordinarily means writing a one-off script against the TecIO API. ``tecaux``
7
+ does this from the command line: it copies the input file verbatim (every zone,
8
+ variable, and existing sharing relationship preserved exactly) and merges in whatever
9
+ new auxiliary entries were requested at each level, in a single read/write pass. An
10
+ existing key with the same name is overwritten; everything else about the source file is
11
+ unchanged.
12
+
13
+ :Usage:
14
+
15
+ .. code:: bash
16
+
17
+ tecaux [-h] [-d KEY=VALUE] ... [-z INDEX KEY=VALUE] ...
18
+ [-v INDEX_OR_NAME KEY=VALUE] ... [-j PATH] [-o PATH] [-f] PATH
19
+
20
+ :Positional Arguments:
21
+ ``PATH``
22
+ Path to the input Tecplot file (``.plt``, ``.szplt``, or ``.dat``).
23
+
24
+ :Options:
25
+ ``-d KEY=VALUE``, ``--data KEY=VALUE``
26
+ A ``name=value`` pair to set as dataset-level auxiliary data. Repeat the flag
27
+ for multiple pairs.
28
+
29
+ ``-z INDEX KEY=VALUE``, ``--zone INDEX KEY=VALUE``
30
+ A ``name=value`` pair to set as zone-level auxiliary data on the one-based zone
31
+ ``INDEX`` -- or on every zone if ``INDEX`` is the literal word ``all``. Repeat
32
+ the flag for multiple pairs and/or multiple zones; each occurrence takes exactly
33
+ one zone and one pair, so ``-z 1 A=1 -z 1 B=2`` sets both ``A`` and ``B`` on
34
+ zone 1.
35
+
36
+ ``-v INDEX_OR_NAME KEY=VALUE``, ``--var INDEX_OR_NAME KEY=VALUE``
37
+ A ``name=value`` pair to set as variable-level auxiliary data on the variable
38
+ given by a one-based index or a name (case-insensitive) -- or on every variable
39
+ if the target is the literal word ``all``. Repeatable, same as ``-z``.
40
+
41
+ ``-j PATH``, ``--json PATH``
42
+ Load bulk auxiliary data from a JSON file (see format below). Applied before
43
+ any ``-d``/``-z``/``-v`` flags, which take precedence on a key collision.
44
+
45
+ ``-o PATH``, ``--output PATH``
46
+ Output file path. The extension controls the output format: ``.szplt``,
47
+ ``.plt``, or ``.dat``. Defaults to ``<stem>_aux<ext>`` in the same directory as
48
+ the input file.
49
+
50
+ ``-f``, ``--force``
51
+ Overwrite the output file if it already exists. Without this flag the command
52
+ exits with an error rather than silently clobbering an existing file.
53
+
54
+ :JSON Format:
55
+ .. code:: json
56
+
57
+ {
58
+ "AUXDATASET": {"Solver": "MyCFD", "Version": "2.1"},
59
+ "AUXZONE": {
60
+ "1": {"Description": "Wing"},
61
+ "all": {"Batch": "2024"}
62
+ },
63
+ "AUXVAR": {
64
+ "Pressure": {"Units": "Pa"},
65
+ "1": {"Source": "Experiment"}
66
+ }
67
+ }
68
+
69
+ All three top-level keys are optional, and match this library's own attribute/method
70
+ names for the same three levels exactly
71
+ (``Write.auxdataset``/``add_auxdataset_dict``, ``Write.auxvar``/``add_auxvar_dict``;
72
+ ``AUXZONE`` is the natural third member of that family even though zone-level aux
73
+ has no "zone"-prefixed name internally.
74
+
75
+ In ``"AUXZONE"``/``"AUXVAR"``, a key is either a one-based index, a variable name
76
+ (``"AUXVAR"`` only), or the literal string ``"all"`` meaning every zone/variable
77
+ (the same three forms ``-z``/``-v`` accept on the command line). Every JSON key must
78
+ be a quoted string, including numeric indices (``"1"``, not ``1``).
79
+
80
+ :Returns:
81
+ A new Tecplot file written to the output path with the requested auxiliary data
82
+ merged in. Exit code is ``0`` on success and non-zero if the input file cannot be
83
+ read, a ``-z``/``-v``/JSON target cannot be resolved, or the output file already
84
+ exists and ``--force`` is not set.
85
+
86
+ Examples:
87
+ Tag a dataset with solver metadata (repeat the flag for multiple pairs)::
88
+
89
+ $ tecaux -d Solver=MyCFD -d Version=2.1 flow.szplt
90
+
91
+ Two pairs on zone 1, one pair on zone 2::
92
+
93
+ $ tecaux -z 1 Description=Wing -z 1 Area=120sqm -z 2 Description=Fuselage \
94
+ flow.szplt
95
+
96
+ Annotate a single variable by name::
97
+
98
+ $ tecaux -v Pressure Units=Pa flow.szplt
99
+
100
+ Every zone at once, via the "all" target::
101
+
102
+ $ tecaux -z all RunDate=2024-01-15 flow.plt
103
+
104
+ Everything in one pass, written as a bash script would naturally lay it out::
105
+
106
+ $ tecaux --data Solver=MyCFD \
107
+ --data Version=2.1 \
108
+ --zone 1 Case=A \
109
+ --zone 1 Description=Wing \
110
+ --zone 2 Case=B \
111
+ --var Pressure Units=Pa \
112
+ -o tagged.szplt flow.szplt
113
+
114
+ Bulk metadata from a file, with one CLI override on top::
115
+
116
+ $ tecaux -j metadata.json -d Version=2.2 flow.szplt
117
+
118
+ Call directly from a Python session::
119
+
120
+ import tecio.cli.tecaux.main as tecaux
121
+
122
+ tecaux(["-d", "Solver=MyCFD", "flow.szplt"])
123
+
124
+ See Also:
125
+ * :mod:`tecio.cli.tecfix` - Rewrite a file with invalid variable arrays set to
126
+ passive, using the same verbatim zone-copy approach.
127
+ * :mod:`tecio.cli.teconvert` - Convert between formats without modifying auxiliary
128
+ data.
129
+
130
+ """
131
+
132
+ from __future__ import annotations
133
+
134
+ import argparse
135
+ import json
136
+ import sys
137
+ from collections.abc import Sequence
138
+ from pathlib import Path
139
+ from typing import Any
140
+
141
+ import numpy as np
142
+
143
+ from .. import open as tecio_open
144
+ from ..libtecio import ZoneType
145
+
146
+ # --------------------------------------------------------------------------------------
147
+ # Constants
148
+ # --------------------------------------------------------------------------------------
149
+
150
+ #: FE zone types that the Write API cannot copy.
151
+ _FE_POLY: frozenset[ZoneType] = frozenset({ZoneType.FEPOLYGON, ZoneType.FEPOLYHEDRON})
152
+
153
+
154
+ class _ArgError(Exception):
155
+ """Raised for a malformed KEY=VALUE pair or unresolvable target."""
156
+
157
+
158
+ # --------------------------------------------------------------------------------------
159
+ # Argument parsing
160
+ # --------------------------------------------------------------------------------------
161
+
162
+
163
+ def _parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
164
+ parser = argparse.ArgumentParser(
165
+ prog="tecaux",
166
+ description=(
167
+ # -|-------------------|---------------------------------------------|
168
+ "Add dataset-, zone-, or variable-level auxiliary data to a Tecplot\n"
169
+ "file in a single read/write pass. Everything else (zones, variables,\n"
170
+ "and sharing) is copied verbatim."
171
+ ),
172
+ epilog=(
173
+ # -|-------------------|---------------------------------------------|
174
+ "Example usage:\n"
175
+ " Dataset-level metadata (repeat the flag for multiple pairs)\n"
176
+ " $ tecaux -d Solver=MyCFD -d Version=2.1 <file>\n"
177
+ " Two pairs on zone 1, one pair on zone 2\n"
178
+ " $ tecaux -z 1 Description=Wing -z 1 Area=120sqm -z 2 Case=B <file>\n"
179
+ " A single variable by name\n"
180
+ " $ tecaux -v Pressure Units=Pa <file>\n"
181
+ " Every zone at once\n"
182
+ " $ tecaux -z all RunDate=2024-01-15 <file>\n"
183
+ " Bulk metadata from a file\n"
184
+ " $ tecaux -j metadata.json <file>\n"
185
+ ),
186
+ formatter_class=lambda prog: argparse.RawDescriptionHelpFormatter(
187
+ prog, width=70, max_help_position=24
188
+ ),
189
+ )
190
+ parser.add_argument(
191
+ "filename",
192
+ type=str,
193
+ help="Input Tecplot file to add auxiliary data to.",
194
+ )
195
+ parser.add_argument(
196
+ "-d",
197
+ "--data",
198
+ action="append",
199
+ default=None,
200
+ metavar="KEY=VALUE",
201
+ help="A name=value pair to set as dataset-level auxiliary data. Repeatable.",
202
+ )
203
+ parser.add_argument(
204
+ "-z",
205
+ "--zone",
206
+ action="append",
207
+ nargs=2,
208
+ default=None,
209
+ metavar=("INDEX", "KEY=VALUE"),
210
+ help=(
211
+ "A name=value pair to set as zone-level auxiliary data on the "
212
+ "given one-based zone index, or on every zone if INDEX is "
213
+ "'all'. Repeatable, one pair per occurrence."
214
+ ),
215
+ )
216
+ parser.add_argument(
217
+ "-v",
218
+ "--var",
219
+ action="append",
220
+ nargs=2,
221
+ default=None,
222
+ metavar=("INDEX_OR_NAME", "KEY=VALUE"),
223
+ help=(
224
+ "A name=value pair to set as variable-level auxiliary data on "
225
+ "the given variable (1-based index or name), or on every "
226
+ "variable if the target is 'all'. Repeatable, one pair per "
227
+ "occurrence."
228
+ ),
229
+ )
230
+ parser.add_argument(
231
+ "-j",
232
+ "--json",
233
+ type=str,
234
+ default=None,
235
+ metavar="PATH",
236
+ help=(
237
+ "Load bulk auxiliary data from a JSON file, applied before "
238
+ "-d/-z/-v (which override it on a key collision). Top-level "
239
+ "keys: AUXDATASET, AUXZONE, AUXVAR (matching Write.auxdataset/"
240
+ "Write.auxvar). Full format in the module docstring."
241
+ ),
242
+ )
243
+ parser.add_argument(
244
+ "-o",
245
+ "--output",
246
+ type=str,
247
+ default=None,
248
+ metavar="PATH",
249
+ help=(
250
+ "Output file path. The extension controls the output format. "
251
+ "Defaults to <stem>_aux<ext> in the same directory as the input."
252
+ ),
253
+ )
254
+ parser.add_argument(
255
+ "-f",
256
+ "--force",
257
+ action="store_true",
258
+ default=False,
259
+ help="Overwrite the output file if it already exists.",
260
+ )
261
+ return parser.parse_args(argv)
262
+
263
+
264
+ # ---------------------------------------------------------------------------
265
+ # Helpers
266
+ # ---------------------------------------------------------------------------
267
+
268
+
269
+ def _parse_kv(token: str) -> tuple[str, str]:
270
+ """Parse one ``KEY=VALUE`` token.
271
+
272
+ Args:
273
+ token: A single string like ``"Solver=MyCFD"``. Only the first ``"="`` is
274
+ significant, so a value may itself contain ``"="``
275
+ (e.g. ``"Formula=a=b+c"``). A value containing spaces must be quoted as
276
+ a whole on the command line, the same as any other shell argument
277
+ (e.g. ``-z 1 "Description=Wing surface"``) -- this function only ever
278
+ sees whatever single token the shell already produced, so it can't
279
+ recover a value that arrived pre-split into several tokens.
280
+
281
+ Returns:
282
+ ``(key, value)``, with the key stripped of surrounding whitespace.
283
+
284
+ Raises:
285
+ _ArgError: If *token* has no ``"="``, or the key is empty.
286
+
287
+ """
288
+ if "=" not in token:
289
+ raise _ArgError(f"Expected KEY=VALUE, got: {token!r}")
290
+ key, _, value = token.partition("=")
291
+ key = key.strip()
292
+ if not key:
293
+ raise _ArgError(f"Empty key in: {token!r}")
294
+ return key, value
295
+
296
+
297
+ def _resolve_variable(spec: str, var_names: list[str]) -> int | None:
298
+ """Return a 0-based variable index from a name or 1-based integer string.
299
+
300
+ Args:
301
+ spec: User-supplied string (e.g. ``"3"`` or ``"Pressure"``).
302
+ var_names: Ordered list of variable names from the reader.
303
+
304
+ Returns:
305
+ 0-based variable index, or ``None`` if *spec* cannot be resolved. Callers are
306
+ responsible for reporting the failure.
307
+
308
+ """
309
+ try:
310
+ idx = int(spec)
311
+ except ValueError:
312
+ pass
313
+ else:
314
+ if idx < 1 or idx > len(var_names):
315
+ return None
316
+ return idx - 1
317
+
318
+ spec_lower = spec.lower()
319
+ for i, name in enumerate(var_names):
320
+ if name.lower() == spec_lower:
321
+ return i
322
+
323
+ return None
324
+
325
+
326
+ def _to_groups(
327
+ raw_pairs: list[list[str]] | None,
328
+ ) -> list[tuple[str | None, dict[str, str]]]:
329
+ """Convert argparse's ``[[target, "KEY=VALUE"], ...]`` into consolidation input.
330
+
331
+ Each occurrence becomes its own single-pair group; ``_consolidate_groups`` merges
332
+ repeats of the same target on its own, so nothing needs to be pre-grouped here.
333
+
334
+ Args:
335
+ raw_pairs: ``args.zone``/``args.var`` as argparse produced them, or ``None`` if
336
+ the flag was never given.
337
+
338
+ Returns:
339
+ ``[(target, {key: value}), ...]``, target ``None`` for ``"all"``.
340
+
341
+ Raises:
342
+ _ArgError: If a KEY=VALUE token is malformed.
343
+
344
+ """
345
+ groups: list[tuple[str | None, dict[str, str]]] = []
346
+ for target_raw, kv_raw in raw_pairs or []:
347
+ target = None if target_raw.lower() == "all" else target_raw
348
+ key, value = _parse_kv(kv_raw)
349
+ groups.append((target, {key: value}))
350
+ return groups
351
+
352
+
353
+ def _consolidate_groups(
354
+ groups: list[tuple[str | None, dict[str, str]]],
355
+ resolve: Any,
356
+ ) -> tuple[dict[str, str], dict[int, dict[str, str]]]:
357
+ """Merge repeated groups into one broadcast dict and one per-target dict.
358
+
359
+ Args:
360
+ groups: ``[(target, {key: value}), ...]`` -- target is ``None`` ("every
361
+ zone"/"every variable") or a raw string to resolve.
362
+ resolve: Callable taking the raw target string and returning a 1-based index for
363
+ a specific target, or raising ``_ArgError`` if it can't be resolved.
364
+
365
+ Returns:
366
+ ``(broadcast, by_target)`` where ``by_target`` is keyed by whatever index
367
+ ``resolve`` returns.
368
+
369
+ """
370
+ broadcast: dict[str, str] = {}
371
+ by_target: dict[int, dict[str, str]] = {}
372
+ for target, pairs in groups:
373
+ if target is None:
374
+ broadcast.update(pairs)
375
+ else:
376
+ idx = resolve(target)
377
+ by_target.setdefault(idx, {}).update(pairs)
378
+ return broadcast, by_target
379
+
380
+
381
+ # --------------------------------------------------------------------------------------
382
+ # JSON loading
383
+ # --------------------------------------------------------------------------------------
384
+
385
+
386
+ def _load_json_aux(
387
+ path: str,
388
+ ) -> tuple[
389
+ dict[str, str],
390
+ list[tuple[str | None, dict[str, str]]],
391
+ list[tuple[str | None, dict[str, str]]],
392
+ ]:
393
+ """Load bulk auxiliary data from a JSON file.
394
+
395
+ Top-level keys are ``AUXDATASET``, ``AUXZONE``, ``AUXVAR``.
396
+
397
+ Returns:
398
+ ``(dataset_aux, zone_groups, var_groups)`` in the same shape ``_to_groups``
399
+ produces, so both sources merge identically.
400
+
401
+ Raises:
402
+ _ArgError: If the file can't be read or parsed, contains an unrecognized
403
+ top-level key, or a value isn't a string -> string mapping.
404
+
405
+ """
406
+ try:
407
+ with open(path, encoding="utf-8") as fh:
408
+ data = json.load(fh)
409
+ except (OSError, json.JSONDecodeError) as exc:
410
+ raise _ArgError(f"Could not read JSON file {path!r}: {exc}") from exc
411
+
412
+ if not isinstance(data, dict):
413
+ raise _ArgError(f"JSON file {path!r} must contain an object at the top level.")
414
+
415
+ _KNOWN_KEYS = {"AUXDATASET", "AUXZONE", "AUXVAR"}
416
+ unknown: set[str] = {str(k) for k in data} - _KNOWN_KEYS
417
+ if unknown:
418
+ raise _ArgError(
419
+ f"Unrecognized top-level key(s) in {path!r}: {sorted(unknown)}. "
420
+ f"Expected one of: {sorted(_KNOWN_KEYS)}."
421
+ )
422
+
423
+ def _as_str_dict(obj: Any, where: str) -> dict[str, str]:
424
+ if not isinstance(obj, dict):
425
+ raise _ArgError(f"{where} must be an object of name: value strings.")
426
+ return {str(k): str(v) for k, v in obj.items()}
427
+
428
+ dataset_aux = _as_str_dict(data.get("AUXDATASET", {}), "'AUXDATASET'")
429
+
430
+ zone_groups: list[tuple[str | None, dict[str, str]]] = []
431
+ for key, val in data.get("AUXZONE", {}).items():
432
+ target = None if str(key).lower() == "all" else str(key)
433
+ zone_groups.append((target, _as_str_dict(val, f"'AUXZONE.{key}'")))
434
+
435
+ var_groups: list[tuple[str | None, dict[str, str]]] = []
436
+ for key, val in data.get("AUXVAR", {}).items():
437
+ target = None if str(key).lower() == "all" else str(key)
438
+ var_groups.append((target, _as_str_dict(val, f"'AUXVAR.{key}'")))
439
+
440
+ return dataset_aux, zone_groups, var_groups
441
+
442
+
443
+ # --------------------------------------------------------------------------------------
444
+ # Per-zone processing
445
+ # --------------------------------------------------------------------------------------
446
+
447
+
448
+ def _process_zone(
449
+ zone: Any,
450
+ ) -> tuple[list[np.ndarray], list[Any], list[bool], list[int], dict[str, str]]:
451
+ """Copy one zone's variable data/metadata verbatim.
452
+
453
+ Sharing references are passed through unchanged: this tool writes every zone in the
454
+ same order as the source, so source zone N is always output zone N.
455
+
456
+ Returns:
457
+ A 5-tuple: ``(writer_data, writer_locs, passive_vars, var_sharing,
458
+ existing_aux)``, where the first two are filtered to active, non-shared
459
+ variables only.
460
+ """
461
+ active_data: list[np.ndarray] = []
462
+ active_locs: list[Any] = []
463
+ passive_vars: list[bool] = []
464
+ var_sharing: list[int] = []
465
+
466
+ for var in zone.variable:
467
+ is_passive = var.is_passive()
468
+ sv = var.shared_zone # 1-based source zone index, or None
469
+ share_int = sv if sv is not None else 0
470
+
471
+ passive_vars.append(is_passive)
472
+ var_sharing.append(share_int)
473
+ active_locs.append(var.value_location)
474
+
475
+ if is_passive or share_int != 0:
476
+ active_data.append(np.array([], dtype=np.float32))
477
+ continue
478
+
479
+ arr = var.values
480
+ if arr is None or arr.size == 0:
481
+ passive_vars[-1] = True
482
+ active_data.append(np.array([], dtype=np.float32))
483
+ else:
484
+ active_data.append(arr)
485
+
486
+ writer_data = [
487
+ arr
488
+ for arr, is_p, sv in zip(active_data, passive_vars, var_sharing, strict=False)
489
+ if not is_p and sv == 0
490
+ ]
491
+ writer_locs = [
492
+ loc
493
+ for loc, is_p, sv in zip(active_locs, passive_vars, var_sharing, strict=False)
494
+ if not is_p and sv == 0
495
+ ]
496
+
497
+ existing_aux: dict[str, str] = {}
498
+ if len(zone.auxdata) > 0:
499
+ existing_aux = dict(zone.auxdata.items())
500
+
501
+ return writer_data, writer_locs, passive_vars, var_sharing, existing_aux
502
+
503
+
504
+ # --------------------------------------------------------------------------------------
505
+ # Main entry point
506
+ # --------------------------------------------------------------------------------------
507
+
508
+
509
+ def main(argv: Sequence[str] | None = None) -> int:
510
+ """Add auxiliary data to a Tecplot file.
511
+
512
+ Returns:
513
+ Exit code -- ``0`` on success, ``1`` on error.
514
+
515
+ """
516
+ args = _parse_args(argv)
517
+
518
+ src = Path(args.filename)
519
+ if not src.exists():
520
+ print(f"Error: input file not found: {src}", file=sys.stderr)
521
+ return 1
522
+
523
+ dst = (
524
+ Path(args.output)
525
+ if args.output is not None
526
+ else src.with_stem(src.stem + "_aux")
527
+ )
528
+
529
+ if dst.exists() and not args.force:
530
+ print(
531
+ f"Error: output file already exists: {dst}\nUse --force to overwrite.",
532
+ file=sys.stderr,
533
+ )
534
+ return 1
535
+
536
+ try:
537
+ # JSON is the baseline; -d/-z/-v groups are appended after it, so they naturally
538
+ # win on a key collision.
539
+ dataset_aux: dict[str, str] = {}
540
+ zone_groups: list[tuple[str | None, dict[str, str]]] = []
541
+ var_groups: list[tuple[str | None, dict[str, str]]] = []
542
+
543
+ if args.json is not None:
544
+ j_dataset, j_zones, j_vars = _load_json_aux(args.json)
545
+ dataset_aux.update(j_dataset)
546
+ zone_groups.extend(j_zones)
547
+ var_groups.extend(j_vars)
548
+
549
+ for kv in args.data or []:
550
+ k, v = _parse_kv(kv)
551
+ dataset_aux[k] = v
552
+ zone_groups.extend(_to_groups(args.zone))
553
+ var_groups.extend(_to_groups(args.var))
554
+ except _ArgError as exc:
555
+ print(f"Error: {exc}", file=sys.stderr)
556
+ return 1
557
+
558
+ if not (dataset_aux or zone_groups or var_groups):
559
+ print(
560
+ "Warning: no -d, -z, -v, or -j given -- output will be a verbatim copy.",
561
+ file=sys.stderr,
562
+ )
563
+
564
+ try:
565
+ with tecio_open(str(src), "r") as reader:
566
+ var_names: list[str] = reader.variables
567
+ num_vars: int = reader.num_vars
568
+ num_zones: int = reader.num_zones
569
+
570
+ def _resolve_zone_target(raw: str) -> int:
571
+ try:
572
+ idx = int(raw)
573
+ except ValueError as exc:
574
+ raise _ArgError(
575
+ f"Zone target must be an index or 'all', got: {raw!r}"
576
+ ) from exc
577
+ if idx < 1 or idx > num_zones:
578
+ raise _ArgError(f"Zone index {idx} out of range [1, {num_zones}].")
579
+ return idx
580
+
581
+ def _resolve_var_target(raw: str) -> int:
582
+ idx0 = _resolve_variable(raw, var_names)
583
+ if idx0 is None:
584
+ raise _ArgError(
585
+ f"Could not resolve variable target {raw!r}. "
586
+ f"Available: {var_names}"
587
+ )
588
+ return idx0 + 1 # keyed 1-based, matching zone targets
589
+
590
+ try:
591
+ zone_broadcast, zone_by_target = _consolidate_groups(
592
+ zone_groups, _resolve_zone_target
593
+ )
594
+ var_broadcast, var_by_target = _consolidate_groups(
595
+ var_groups, _resolve_var_target
596
+ )
597
+ except _ArgError as exc:
598
+ print(f"Error: {exc}", file=sys.stderr)
599
+ return 1
600
+
601
+ print(f"Adding auxiliary data: {src} -> {dst}")
602
+ if dataset_aux:
603
+ print(f" Dataset : {dataset_aux}")
604
+ for target, pairs in zone_groups:
605
+ where = f"zone {target}" if target is not None else "every zone"
606
+ print(f" Zone : {pairs} ({where})")
607
+ for target, pairs in var_groups:
608
+ where = f"variable {target}" if target is not None else "every variable"
609
+ print(f" Variable: {pairs} ({where})")
610
+
611
+ with tecio_open(
612
+ str(dst),
613
+ "w",
614
+ title=reader.title,
615
+ variables=var_names,
616
+ file_type=reader.file_type,
617
+ ) as writer:
618
+ merged_dataset_aux = {**dict(reader.auxdata.items()), **dataset_aux}
619
+ if merged_dataset_aux:
620
+ writer.add_auxdataset_dict(merged_dataset_aux)
621
+
622
+ auxvar: dict[int, dict[str, str]] = {}
623
+ for i in range(num_vars):
624
+ one_based = i + 1
625
+ existing = dict(reader.get_var_auxdata(one_based).items())
626
+ merged = {
627
+ **existing,
628
+ **var_broadcast,
629
+ **var_by_target.get(one_based, {}),
630
+ }
631
+ if merged:
632
+ auxvar[one_based] = merged
633
+ if auxvar:
634
+ writer.add_auxvar_dict(auxvar)
635
+
636
+ # add_auxdataset_dict()/add_auxvar_dict() only buffer; the write happens
637
+ # in flush_aux(), normally auto-triggered by the *lazy*-open path on the
638
+ # first zone write. Passing variables= above means this writer is
639
+ # already open (eager), so that automatic trigger never fires.
640
+ writer.flush_aux()
641
+
642
+ for i, zone in enumerate(reader.zone):
643
+ zone_num = i + 1
644
+ zt = zone.zone_type
645
+
646
+ if zt in _FE_POLY:
647
+ print(
648
+ f"Warning: zone {zone_num} ('{zone.title}') is "
649
+ f"{zt.name} and cannot be copied -- skipping.",
650
+ file=sys.stderr,
651
+ )
652
+ continue
653
+
654
+ (
655
+ writer_data,
656
+ writer_locs,
657
+ passive_vars,
658
+ var_sharing,
659
+ existing_zone_aux,
660
+ ) = _process_zone(zone)
661
+
662
+ merged_zone_aux = {
663
+ **existing_zone_aux,
664
+ **zone_broadcast,
665
+ **zone_by_target.get(zone_num, {}),
666
+ } or None
667
+
668
+ common_kw: dict[str, Any] = dict(
669
+ title=zone.title,
670
+ value_locations=writer_locs,
671
+ passive_vars=passive_vars,
672
+ var_sharing=var_sharing,
673
+ solution_time=zone.solution_time,
674
+ strand_id=zone.strand_id,
675
+ aux=merged_zone_aux,
676
+ )
677
+
678
+ if zt == ZoneType.ORDERED:
679
+ writer.write_ijk_zone(data=writer_data, **common_kw)
680
+ else:
681
+ con_sharing = zone.shared_connectivity
682
+ writer.write_fe_zone(
683
+ zone_type=zt,
684
+ data=writer_data,
685
+ node_map=None if con_sharing else zone.node_map,
686
+ con_sharing=con_sharing,
687
+ **common_kw,
688
+ )
689
+
690
+ except Exception as exc: # noqa: BLE001
691
+ print(f"Error: {exc}", file=sys.stderr)
692
+ dst.unlink(missing_ok=True)
693
+ return 1
694
+
695
+ print(f"Done. Output written to: {dst}")
696
+ return 0
697
+
698
+
699
+ if __name__ == "__main__":
700
+ sys.exit(main())
tecio/dat/_read.py CHANGED
@@ -1011,8 +1011,12 @@ class Read:
1011
1011
  self._zones: list[ReadZone] = []
1012
1012
  self._zone_list: ZoneList[ReadZone] | None = None
1013
1013
  self._auxdata: ReadAuxData = ReadAuxData()
1014
- # Index 0 is a None placeholder so that 1-based indexing works directly.
1014
+ # Index 0 is a None placeholder so that 1-based indexing works directly
1015
1015
  self._var_auxdata: list[ReadAuxData | None] = [None]
1016
+ # Raw VARAUXDATA lines seen before the first zone, buffered by
1017
+ # _parse_file_header() and applied once _var_auxdata is allocated with the
1018
+ # correct length (num_vars isn't known until the header finishes parsing)
1019
+ self._deferred_var_aux_lines: list[str] = []
1016
1020
  self._parse()
1017
1021
 
1018
1022
  def __repr__(self) -> str:
@@ -1148,6 +1152,12 @@ class Read:
1148
1152
  # Build per-variable aux data slots now that num_vars is known.
1149
1153
  self._var_auxdata = [None] + [ReadAuxData() for _ in range(self.num_vars)]
1150
1154
 
1155
+ # Now that _var_auxdata exists, apply any VARAUXDATA lines that appeared before
1156
+ # the first zone
1157
+ for raw in self._deferred_var_aux_lines:
1158
+ _apply_varauxdata(raw, self._var_auxdata)
1159
+ self._deferred_var_aux_lines.clear()
1160
+
1151
1161
  while tokens.has_more():
1152
1162
  line = tokens.peek_stripped()
1153
1163
  upper = line.upper()
@@ -1229,9 +1239,11 @@ class Read:
1229
1239
  if name:
1230
1240
  self._auxdata._data[name] = value
1231
1241
 
1232
- # VARAUXDATA lines before the first ZONE are deferred — they need the
1233
- # variable list, which may not yet be complete. They are processed in the
1234
- # main _parse() loop after the header finishes.
1242
+ elif upper_key.startswith("VARAUXDATA"):
1243
+ # Can't process this without num_vars, and therefore _var_auxdata, isn't
1244
+ # known until this header finishes parsing. Buffer the raw line;
1245
+ # _parse() applies these once _var_auxdata is allocated.
1246
+ self._deferred_var_aux_lines.append(line)
1235
1247
 
1236
1248
  def _parse_zone(self, tokens: _LineBuffer) -> None:
1237
1249
  """Parse one ZONE block (header + data blocks + connectivity).
tecio/libtecio.py CHANGED
@@ -693,6 +693,12 @@ lib.tecFileWriterClose.restype = ctypes.c_int32
693
693
  lib.tecFileWriterClose.argtypes = [
694
694
  ctypes.POINTER(ctypes.c_void_p),
695
695
  ]
696
+ lib.tecFileWriterFlush.restype = ctypes.c_int32
697
+ lib.tecFileWriterFlush.argtypes = [
698
+ ctypes.c_void_p, # fileHandle
699
+ ctypes.c_int32, # numZonesToRetain
700
+ ctypes.POINTER(ctypes.c_int32), # zonesToRetain
701
+ ]
696
702
 
697
703
  # Write Zone Headers
698
704
  lib.tecZoneCreateIJK.restype = ctypes.c_int32
@@ -1071,7 +1077,7 @@ lib.tecusr142.argtypes = [
1071
1077
  # --------------------------------------------------------------------------------------
1072
1078
 
1073
1079
 
1074
- # Reading SZL files
1080
+ # -- Reading SZL files -----------------------------------------------------------------
1075
1081
  def tec_file_reader_open(file_name: str) -> ctypes.c_void_p:
1076
1082
  """Open an SZL file for reading.
1077
1083
 
@@ -1204,7 +1210,7 @@ def tec_data_set_get_num_zones(handle: ctypes.c_void_p) -> int:
1204
1210
  return num_zones.value
1205
1211
 
1206
1212
 
1207
- # Reading SZL zones
1213
+ # -- Reading SZL zones -----------------------------------------------------------------
1208
1214
  def tec_zone_get_ijk(handle: ctypes.c_void_p, zone_index: int) -> tuple[int, int, int]:
1209
1215
  """Get zone dimensions (ORDERED) or node/element counts (FE).
1210
1216
 
@@ -1488,7 +1494,7 @@ def tec_zone_node_map_get(
1488
1494
  return np.ctypeslib.as_array(nodemap).reshape(num_elements, nodes_per_cell)
1489
1495
 
1490
1496
 
1491
- # Reading SZL variable data
1497
+ # -- Reading SZL variable data ---------------------------------------------------------
1492
1498
  def tec_var_get_name(handle: ctypes.c_void_p, var_index: int) -> str:
1493
1499
  """Get a variable name by index.
1494
1500
 
@@ -1962,7 +1968,7 @@ def tec_zone_var_get_uint8_values(
1962
1968
  return np.ctypeslib.as_array(values)
1963
1969
 
1964
1970
 
1965
- # Reading SZL aux data
1971
+ # -- Reading SZL aux data --------------------------------------------------------------
1966
1972
  def tec_data_set_aux_data_get_num_items(handle: ctypes.c_void_p) -> int:
1967
1973
  """Get the number of dataset-level auxiliary data items.
1968
1974
 
@@ -2157,7 +2163,7 @@ def tec_zone_aux_data_get_item(
2157
2163
  # --------------------------------------------------------------------------------------
2158
2164
 
2159
2165
 
2160
- # Initialization and File Handling
2166
+ # -- Initialization and File Handling --------------------------------------------------
2161
2167
  def tec_file_writer_open(
2162
2168
  filename: str,
2163
2169
  variables: Sequence[str],
@@ -2225,7 +2231,58 @@ def tec_file_writer_close(handle: ctypes.c_void_p) -> None:
2225
2231
  )
2226
2232
 
2227
2233
 
2228
- # Write Zone Headers
2234
+ def tec_file_writer_flush(
2235
+ handle: ctypes.c_void_p,
2236
+ num_zones_to_retain: int = 0,
2237
+ zones_to_retain: Sequence[int] | None = None,
2238
+ ) -> None:
2239
+ """Flush written zone data to a temporary intermediate file.
2240
+
2241
+ Args:
2242
+ handle (ctypes.c_void_p): Writer handle.
2243
+ num_zones_to_retain (int): Number of zones to keep in memory.
2244
+ zones_to_retain (Sequence[int] | None): 1-based zone indices to retain.
2245
+
2246
+ Raises:
2247
+ TecioError: On C library error.
2248
+
2249
+ Important:
2250
+ SZL Only!
2251
+
2252
+ Note:
2253
+ Used to reduce memory usage for large files. All zone data written so far, other
2254
+ than any zones listed in ``zones_to_retain``, is written out to a temporary file
2255
+ on disk and released from memory.
2256
+
2257
+ Note:
2258
+ Retained zones can still be modified.
2259
+
2260
+ Note:
2261
+ Temporary files created by flushing are merged into the final output file when
2262
+ :func:`tec_file_writer_close` is called.
2263
+ """
2264
+ zones_ptr = None
2265
+ if zones_to_retain is not None and len(zones_to_retain) > 0:
2266
+ zones_array = (ctypes.c_int32 * len(zones_to_retain))(*zones_to_retain)
2267
+ zones_ptr = ctypes.cast(zones_array, ctypes.POINTER(ctypes.c_int32))
2268
+ else:
2269
+ zones_ptr = ctypes.POINTER(ctypes.c_int32)()
2270
+
2271
+ ret = lib.tecFileWriterFlush(
2272
+ handle,
2273
+ ctypes.c_int32(num_zones_to_retain),
2274
+ zones_ptr,
2275
+ )
2276
+ if ret != 0:
2277
+ raise TecioError(
2278
+ f"tecFileWriterFlush Error: handle={handle}, "
2279
+ f"num_zones_to_retain={num_zones_to_retain}, "
2280
+ f"zones_to_retain={zones_to_retain}, "
2281
+ f"return_code={ret}"
2282
+ )
2283
+
2284
+
2285
+ # -- Write Zone Headers ----------------------------------------------------------------
2229
2286
  def tec_zone_create_ijk(
2230
2287
  handle: ctypes.c_void_p,
2231
2288
  zone_title: str,
@@ -2417,7 +2474,7 @@ def tec_zone_create_fe(
2417
2474
  return zone_out.value
2418
2475
 
2419
2476
 
2420
- # Optional fields
2477
+ # -- Optional fields -------------------------------------------------------------------
2421
2478
  def tec_zone_set_unsteady_options(
2422
2479
  handle: ctypes.c_void_p, zone: int, strand: int = 0, solution_time: float = 0.0
2423
2480
  ) -> None:
@@ -2532,7 +2589,7 @@ def tec_zone_add_aux_data(
2532
2589
  )
2533
2590
 
2534
2591
 
2535
- # ---- Write variable value functions --------------------------------
2592
+ # -- Write variable value functions ----------------------------------------------------
2536
2593
  def tec_zone_var_write_double_values(
2537
2594
  handle: ctypes.c_void_p, zone: int, var: int, values: npt.ArrayLike
2538
2595
  ) -> None:
@@ -2690,7 +2747,7 @@ def tec_zone_var_write_uint8_values(
2690
2747
  )
2691
2748
 
2692
2749
 
2693
- # Write Zone Connectivity (FE zones only)
2750
+ # -- Write Zone Connectivity (FE zones only) -------------------------------------------
2694
2751
  def tec_zone_node_map_write32(
2695
2752
  handle: ctypes.c_void_p,
2696
2753
  zone: int,
@@ -2889,7 +2946,7 @@ def tec_zone_face_nbr_write_connections64(
2889
2946
  # --------------------------------------------------------------------------------------
2890
2947
 
2891
2948
 
2892
- # File initialization and finalization
2949
+ # -- File initialization and finalization ----------------------------------------------
2893
2950
  def tecini142(
2894
2951
  filename: str,
2895
2952
  variables: Sequence[str],
@@ -3039,7 +3096,7 @@ def tecforeign142(output_foreign_byte_order: int) -> None:
3039
3096
  raise TecioError(f"tecforeign142 Error: return_code={ret}")
3040
3097
 
3041
3098
 
3042
- # Zone creation
3099
+ # -- Zone creation ---------------------------------------------------------------------
3043
3100
  def teczne142(
3044
3101
  zone_title: str,
3045
3102
  zone_type: int | ZoneType,
@@ -3329,7 +3386,7 @@ def tecznefemixed142(
3329
3386
  )
3330
3387
 
3331
3388
 
3332
- # Data writing
3389
+ # -- Data writing ----------------------------------------------------------------------
3333
3390
  def tecdat142(
3334
3391
  field_data: npt.ArrayLike,
3335
3392
  is_double: bool = True,
@@ -3379,7 +3436,7 @@ def tecdat142(
3379
3436
  )
3380
3437
 
3381
3438
 
3382
- # Connectivity writing
3439
+ # -- Connectivity writing --------------------------------------------------------------
3383
3440
  def tecnode142(nodes: npt.ArrayLike) -> None:
3384
3441
  """Write node connectivity for an FE zone (classic API).
3385
3442
 
@@ -3538,7 +3595,7 @@ def tecpolybconn142(
3538
3595
  )
3539
3596
 
3540
3597
 
3541
- # Auxiliary data
3598
+ # -- Auxiliary data --------------------------------------------------------------------
3542
3599
  def tecauxstr142(name: str, value: str) -> None:
3543
3600
  """Add dataset-level auxiliary data (classic API).
3544
3601
 
@@ -3613,7 +3670,7 @@ def teczauxstr142(name: str, value: str) -> None:
3613
3670
  )
3614
3671
 
3615
3672
 
3616
- # User-defined data (custom records)
3673
+ # -- User-defined data (custom records) ------------------------------------------------
3617
3674
  def tecusr142(user_rec: str) -> None:
3618
3675
  """Write a user-defined data record (classic API).
3619
3676
 
tecio/szl/_write.py CHANGED
@@ -599,6 +599,7 @@ class Write:
599
599
  strand_id: int = 0,
600
600
  aux: dict[str, Any] | None = None,
601
601
  datapacking: DataPacking | str = DataPacking.BLOCK,
602
+ flush: bool = False,
602
603
  ) -> None:
603
604
  """Write a complete IJK-ordered zone.
604
605
 
@@ -631,6 +632,12 @@ class Write:
631
632
  default). :attr:`~tecio.libtecio.DataPacking.POINT` is an
632
633
  ASCII-only layout and is not supported by the SZL binary
633
634
  format. Defined only for parity with ASCII writer.
635
+ flush: If ``True``, flush this zone and all previous data to a
636
+ temporary intermediate file immediately after writing,
637
+ releasing data from memory. Defaults to ``False``. Useful
638
+ when memory is a concern, but adds the overhead of a disk
639
+ write. Temporary files are merged back into the final
640
+ output file when :meth:`close` is called.
634
641
 
635
642
  Raises:
636
643
  NotImplementedError: If *datapacking* is
@@ -878,6 +885,10 @@ class Write:
878
885
  dt=dtype,
879
886
  )
880
887
 
888
+ # Flush zone data, releasing memory
889
+ if flush:
890
+ libtecio.tec_file_writer_flush(self._check_handle())
891
+
881
892
  # Finally set zone metadata after successfully completing TecIO calls
882
893
  self._meta.record_zone(
883
894
  ZoneMeta(
@@ -915,6 +926,7 @@ class Write:
915
926
  strand_id: int = 0,
916
927
  aux: dict[str, Any] | None = None,
917
928
  datapacking: DataPacking | str = DataPacking.BLOCK,
929
+ flush: bool = False,
918
930
  ) -> None:
919
931
  """Write a complete finite-element zone.
920
932
 
@@ -970,6 +982,12 @@ class Write:
970
982
  default). :attr:`~tecio.libtecio.DataPacking.POINT` is an
971
983
  ASCII-only layout and is not supported by the SZL binary
972
984
  format. Defined only for parity with ASCII writer.
985
+ flush: If ``True``, flush this zone and all previous data to a
986
+ temporary intermediate file immediately after writing,
987
+ releasing data from memory. Defaults to ``False``. Useful
988
+ when memory is a concern, but adds the overhead of a disk
989
+ write. Temporary files are merged back into the final
990
+ output file when :meth:`close` is called.
973
991
 
974
992
  Raises:
975
993
  NotImplementedError: For FEPOLYGON, FEPOLYHEDRON, or if *datapacking*
@@ -1215,6 +1233,10 @@ class Write:
1215
1233
  face_neighbors_arr,
1216
1234
  )
1217
1235
 
1236
+ # Flush zone data, releasing memory
1237
+ if flush:
1238
+ libtecio.tec_file_writer_flush(self._check_handle())
1239
+
1218
1240
  # Finally set zone metadata after successfully completing TecIO calls
1219
1241
  self._meta.record_zone(
1220
1242
  ZoneMeta(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tecio-python
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Python interface for reading and writing Tecplot data files
5
5
  Project-URL: Homepage, https://github.com/meersman/tecio
6
6
  Project-URL: Documentation, https://meersman.github.io/tecio/
@@ -3,9 +3,10 @@ tecio/_containers.py,sha256=bEW0fu0fiCQnAJNveiaGYbSs1F4lpKOr40ijm5O8UOM,10443
3
3
  tecio/_io.py,sha256=c6bY6nnlKsKAYP7HFCl66wcyzdnQryJHVNCI1DRB7d0,30566
4
4
  tecio/_meta.py,sha256=4_88COTceqI7woPNGeXF_KJ9JlEWLg9jmzsYZJDuHzI,7360
5
5
  tecio/_utils.py,sha256=rPK5w2SxwQQCxRKdwhBDNdl8QTup1HLCCSnC1pzgdRI,8755
6
- tecio/libtecio.py,sha256=iK2aaUAC5flINc8O1FatvNLRblDhDcfr5S-f6MFwWKY,111742
6
+ tecio/libtecio.py,sha256=lFUx9zeNLC4g3TLUta7-UsP8HLc9SdZf38ztw6Kv9lM,114506
7
7
  tecio/cli/__init__.py,sha256=lS6d-vlA64CshzmtrRw3YVGqjMedB-ST7rkbm11UVIA,1090
8
8
  tecio/cli/tec2mat.py,sha256=CypQGKfArMFZg5LkZRpXHQD0PUaKuUSQBqrOLkkxkN8,18178
9
+ tecio/cli/tecaux.py,sha256=ZOmmwVYW33c1pJ9gAXnaZedXODF8h-YKqLJI54Mmiu8,25422
9
10
  tecio/cli/tecdump.py,sha256=Ry3FYSblshgOwjtPfH71536lhKIqjgA1HJqK7zrO4Qo,10949
10
11
  tecio/cli/tecextract.py,sha256=1bqQi6AkabGawslsqUtdOX0__Sx_dD9pUrNCWCHxSbA,15400
11
12
  tecio/cli/tecfix.py,sha256=lwin4d5pPN30ZyVvVfBmRLCkSnURwGMAG_Ny7nMF37w,19460
@@ -15,18 +16,18 @@ tecio/cli/tecscale.py,sha256=jjv2GUQ0CeRvgRTWaVKnk4Ot5iZnlXU8VNUgYJXAg8E,13984
15
16
  tecio/cli/tecslice.py,sha256=_OqRls6HZ_lJ6-COQ9P2G6qVuo0LNBWaLj_pb0UMzTk,34787
16
17
  tecio/cli/tecstats.py,sha256=NXAInnDvv6UveOHuCjFUw2frlrlNnJDwZ6_DrzizK4E,18359
17
18
  tecio/dat/__init__.py,sha256=nkjRnmvevjpxX7fj_iCHAxKrzPIrFqLgpnYDwiLRExY,2672
18
- tecio/dat/_read.py,sha256=Qk7QGMictGOC8GMfxwHuvG0DzYw83-vNO5Ui2450LXk,58779
19
+ tecio/dat/_read.py,sha256=iMcEcLGBgtGeMOqH5lvFOtsmCWOYRbq-aHXZm8hkApM,59452
19
20
  tecio/dat/_write.py,sha256=Bs_HSobk7NvbUFoPlLOQrGpB4wEf3uQjxPPCUT253nM,47729
20
21
  tecio/plt/__init__.py,sha256=13aEKPaShMzV4_5kyohhsmSqeA5Aur0oF6P0DGispSA,1078
21
22
  tecio/plt/_read.py,sha256=EYpSKostFTwLWUZc0-CkdqPnhvmvI35RHnQVgXGVIh4,56811
22
23
  tecio/plt/_write.py,sha256=ugXqzuMe-vHOrZjrwZPVz4izXsDc_7KkjDrXZsX3t2o,46413
23
24
  tecio/szl/__init__.py,sha256=03chcSDXZoBZ3j1vygBkgjjuPqJvth2yoWYbnpdqhHw,1406
24
25
  tecio/szl/_read.py,sha256=rTbwo2FSbc4T8kEKYMpPgK36zsNfVrUZeuWNvBy9Cok,27938
25
- tecio/szl/_write.py,sha256=aSgxZ0E23yGdeBL4v4qt9lpxEOH7dQmvgt6-ajWvuD4,52316
26
- tecio_python-0.2.0.dist-info/licenses/LICENSE,sha256=WYmcYJG1QFgu1hfo7qrEkZ3Jhcz8NUWe6XUraZvlIFs,10172
27
- tecio_python-0.2.0.dist-info/licenses/NOTICE,sha256=94EJ-1hUlQE8JyLtMbaVpmj8EMDjJ1jGr_9OBZQnQ-c,340
28
- tecio_python-0.2.0.dist-info/METADATA,sha256=W9_Fn8nh73vqi6w34TkgKGIL7qvAD5AX1WOu9HmQfb4,5078
29
- tecio_python-0.2.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
30
- tecio_python-0.2.0.dist-info/entry_points.txt,sha256=s_LNBH5m4lCgwxEMcD4wAUKbz5KjlFXFBPzULV_aStY,331
31
- tecio_python-0.2.0.dist-info/top_level.txt,sha256=ayCfhWQNUimgwyaTSyamJ3LbgjloZRWl2vlcZCjYWmQ,6
32
- tecio_python-0.2.0.dist-info/RECORD,,
26
+ tecio/szl/_write.py,sha256=nNOQqHlX9jm1xbNVNouWXp6Y9wbD06vLbWfX5gaCNMM,53624
27
+ tecio_python-0.2.1.dist-info/licenses/LICENSE,sha256=WYmcYJG1QFgu1hfo7qrEkZ3Jhcz8NUWe6XUraZvlIFs,10172
28
+ tecio_python-0.2.1.dist-info/licenses/NOTICE,sha256=94EJ-1hUlQE8JyLtMbaVpmj8EMDjJ1jGr_9OBZQnQ-c,340
29
+ tecio_python-0.2.1.dist-info/METADATA,sha256=YKrvqNOrDqDbdlCdeaiB4ICBIro0VEikrWNDzHC0qD0,5078
30
+ tecio_python-0.2.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
31
+ tecio_python-0.2.1.dist-info/entry_points.txt,sha256=TH2UnRF700KO0GjeRFwkBBtFKN1PyP2tgrc11OsVNAc,362
32
+ tecio_python-0.2.1.dist-info/top_level.txt,sha256=ayCfhWQNUimgwyaTSyamJ3LbgjloZRWl2vlcZCjYWmQ,6
33
+ tecio_python-0.2.1.dist-info/RECORD,,
@@ -1,5 +1,6 @@
1
1
  [console_scripts]
2
2
  tec2mat = tecio.cli.tec2mat:main
3
+ tecaux = tecio.cli.tecaux:main
3
4
  tecdump = tecio.cli.tecdump:main
4
5
  tecextract = tecio.cli.tecextract:main
5
6
  tecfix = tecio.cli.tecfix:main