aemo-mdff-reader 2.0.2__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.
- aemo_mdff_reader/__init__.py +84 -0
- aemo_mdff_reader/aggregate.py +168 -0
- aemo_mdff_reader/cli.py +231 -0
- aemo_mdff_reader/parser.py +1241 -0
- aemo_mdff_reader/py.typed +0 -0
- aemo_mdff_reader/reader.py +107 -0
- aemo_mdff_reader/spec.py +242 -0
- aemo_mdff_reader/sql/__init__.py +22 -0
- aemo_mdff_reader/sql/query.py +102 -0
- aemo_mdff_reader/sql/store.py +178 -0
- aemo_mdff_reader/types.py +437 -0
- aemo_mdff_reader-2.0.2.dist-info/METADATA +247 -0
- aemo_mdff_reader-2.0.2.dist-info/RECORD +17 -0
- aemo_mdff_reader-2.0.2.dist-info/WHEEL +5 -0
- aemo_mdff_reader-2.0.2.dist-info/entry_points.txt +2 -0
- aemo_mdff_reader-2.0.2.dist-info/licenses/LICENSE +21 -0
- aemo_mdff_reader-2.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""aemo-mdff-reader — fast streaming reader for AEMO NEM12 / NEM13 files.
|
|
2
|
+
|
|
3
|
+
Recommended entry points:
|
|
4
|
+
|
|
5
|
+
>>> from aemo_mdff_reader import parse # NEM12 300 records
|
|
6
|
+
>>> from aemo_mdff_reader import parse_accumulations # NEM13 250 records
|
|
7
|
+
>>> from aemo_mdff_reader import parse_all # both, in file order
|
|
8
|
+
|
|
9
|
+
Each iterator is lazy (O(1) memory). For pandas DataFrames, see
|
|
10
|
+
:func:`to_dataframe` (NEM12) and :func:`to_accumulations_dataframe` (NEM13).
|
|
11
|
+
|
|
12
|
+
Spec compatibility: AEMO MDFF v2.6 (effective 29 September 2024). The
|
|
13
|
+
allowed-value tables — quality flags, transaction codes, reason codes,
|
|
14
|
+
units of measure — are exposed as constants in :mod:`aemo_mdff_reader.spec`.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from . import aggregate, spec
|
|
20
|
+
from .parser import (
|
|
21
|
+
NEM12ParseError,
|
|
22
|
+
iter_columns_chunks,
|
|
23
|
+
iter_dataframes,
|
|
24
|
+
nmi_checksum,
|
|
25
|
+
parse,
|
|
26
|
+
parse_accumulations,
|
|
27
|
+
parse_accumulations_to_columns,
|
|
28
|
+
parse_all,
|
|
29
|
+
parse_b2b,
|
|
30
|
+
parse_events,
|
|
31
|
+
parse_header,
|
|
32
|
+
parse_to_columns,
|
|
33
|
+
to_accumulations_dataframe,
|
|
34
|
+
to_columns,
|
|
35
|
+
to_dataframe,
|
|
36
|
+
validate_file,
|
|
37
|
+
validate_nmi,
|
|
38
|
+
write_accumulations_csv,
|
|
39
|
+
write_csv,
|
|
40
|
+
)
|
|
41
|
+
from .reader import INTERVAL_DATA_OUTPUT_HEADERS, NEMReader
|
|
42
|
+
from .types import (
|
|
43
|
+
AccumulationReading,
|
|
44
|
+
B2BDetails,
|
|
45
|
+
Header,
|
|
46
|
+
IntervalEvent,
|
|
47
|
+
IntervalReading,
|
|
48
|
+
NMIDetails,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
__version__ = "2.0.2"
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"INTERVAL_DATA_OUTPUT_HEADERS",
|
|
55
|
+
"AccumulationReading",
|
|
56
|
+
"B2BDetails",
|
|
57
|
+
"Header",
|
|
58
|
+
"IntervalEvent",
|
|
59
|
+
"IntervalReading",
|
|
60
|
+
"NEM12ParseError",
|
|
61
|
+
"NEMReader",
|
|
62
|
+
"NMIDetails",
|
|
63
|
+
"__version__",
|
|
64
|
+
"aggregate",
|
|
65
|
+
"iter_columns_chunks",
|
|
66
|
+
"iter_dataframes",
|
|
67
|
+
"nmi_checksum",
|
|
68
|
+
"parse",
|
|
69
|
+
"parse_accumulations",
|
|
70
|
+
"parse_accumulations_to_columns",
|
|
71
|
+
"parse_all",
|
|
72
|
+
"parse_b2b",
|
|
73
|
+
"parse_events",
|
|
74
|
+
"parse_header",
|
|
75
|
+
"parse_to_columns",
|
|
76
|
+
"spec",
|
|
77
|
+
"to_accumulations_dataframe",
|
|
78
|
+
"to_columns",
|
|
79
|
+
"to_dataframe",
|
|
80
|
+
"validate_file",
|
|
81
|
+
"validate_nmi",
|
|
82
|
+
"write_accumulations_csv",
|
|
83
|
+
"write_csv",
|
|
84
|
+
]
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Post-parse aggregation helpers.
|
|
2
|
+
|
|
3
|
+
Lightweight composable iterators for the most common downstream
|
|
4
|
+
operations on parsed NEM12 data: grouping by NMI/channel, and rolling
|
|
5
|
+
intervals up to daily totals. Each function takes any iterable of
|
|
6
|
+
:class:`IntervalReading` objects, so they compose with :func:`parse`,
|
|
7
|
+
``parse_to_columns``, and the buffered :class:`NEMReader` facade.
|
|
8
|
+
|
|
9
|
+
>>> from aemo_mdff_reader import parse
|
|
10
|
+
>>> from aemo_mdff_reader.aggregate import daily_totals
|
|
11
|
+
>>> for d in daily_totals(parse("metering.csv")):
|
|
12
|
+
... print(d.nmi, d.register_id, d.interval_date, d.total)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import itertools
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from typing import Iterable, Iterator, List, NamedTuple, Optional, Tuple, TypeVar
|
|
20
|
+
|
|
21
|
+
from .types import IntervalReading
|
|
22
|
+
|
|
23
|
+
_T = TypeVar("_T")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def iter_chunks(items: Iterable[_T], size: int) -> Iterator[List[_T]]:
|
|
27
|
+
"""Yield successive lists of ``size`` items from ``items``.
|
|
28
|
+
|
|
29
|
+
Useful for batching streaming reads when the downstream consumer
|
|
30
|
+
works in fixed-size lots — DB ``executemany`` / batch HTTP / etc. —
|
|
31
|
+
without materialising the whole stream in memory. Memory cost is
|
|
32
|
+
O(size), not O(file).
|
|
33
|
+
|
|
34
|
+
The final batch may be shorter than ``size`` if the input is not a
|
|
35
|
+
multiple of it. Empty input yields no batches.
|
|
36
|
+
|
|
37
|
+
>>> from aemo_mdff_reader import parse
|
|
38
|
+
>>> from aemo_mdff_reader.aggregate import iter_chunks
|
|
39
|
+
>>> for batch in iter_chunks(parse("metering.csv"), 5_000):
|
|
40
|
+
... db.executemany(INSERT_SQL, [r.to_dict() for r in batch])
|
|
41
|
+
"""
|
|
42
|
+
if size <= 0:
|
|
43
|
+
raise ValueError(f"chunk size must be positive, got {size}")
|
|
44
|
+
it = iter(items)
|
|
45
|
+
while True:
|
|
46
|
+
chunk = list(itertools.islice(it, size))
|
|
47
|
+
if not chunk:
|
|
48
|
+
return
|
|
49
|
+
yield chunk
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ChannelKey(NamedTuple):
|
|
53
|
+
"""Composite key identifying a single metering channel."""
|
|
54
|
+
|
|
55
|
+
nmi: str
|
|
56
|
+
register_id: str
|
|
57
|
+
nmi_suffix: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class DailyTotal(NamedTuple):
|
|
61
|
+
"""A single channel's total energy / quantity for a calendar day.
|
|
62
|
+
|
|
63
|
+
``total`` is the sum of ``IntervalReading.value`` for every reading
|
|
64
|
+
that fell on ``interval_date``. ``interval_count`` is the number of
|
|
65
|
+
readings summed (useful for spotting partial days). ``unique_quality_flags``
|
|
66
|
+
is the set of distinct 1-character quality flags seen — a single
|
|
67
|
+
``{"A"}`` means the day is fully Actual, anything else hints at
|
|
68
|
+
substituted or estimated data.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
nmi: str
|
|
72
|
+
register_id: str
|
|
73
|
+
nmi_suffix: str
|
|
74
|
+
uom: str
|
|
75
|
+
interval_date: datetime
|
|
76
|
+
total: float
|
|
77
|
+
interval_count: int
|
|
78
|
+
unique_quality_flags: frozenset[str]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def group_by_nmi(
|
|
82
|
+
readings: Iterable[IntervalReading],
|
|
83
|
+
) -> Iterator[Tuple[ChannelKey, Iterator[IntervalReading]]]:
|
|
84
|
+
"""Yield ``(ChannelKey, group)`` pairs, one per (NMI, register, suffix).
|
|
85
|
+
|
|
86
|
+
NEM12 files are emitted in 200-record blocks: every 300 row inside
|
|
87
|
+
a block shares the same NMI / register / suffix, and a new 200 row
|
|
88
|
+
starts a new block. We use :func:`itertools.groupby` over those
|
|
89
|
+
fields, which means each ``group`` iterator must be consumed before
|
|
90
|
+
the next pair is requested — exactly the same contract as
|
|
91
|
+
``itertools.groupby``.
|
|
92
|
+
|
|
93
|
+
For a materialised list per group, wrap the inner iterator::
|
|
94
|
+
|
|
95
|
+
for key, group in group_by_nmi(parse(path)):
|
|
96
|
+
rows = list(group)
|
|
97
|
+
...
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def _key(r: IntervalReading) -> ChannelKey:
|
|
101
|
+
return ChannelKey(r.nmi, r.register_id, r.nmi_suffix)
|
|
102
|
+
|
|
103
|
+
yield from itertools.groupby(readings, key=_key)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def daily_totals(readings: Iterable[IntervalReading]) -> Iterator[DailyTotal]:
|
|
107
|
+
"""Yield one :class:`DailyTotal` per (channel, calendar day).
|
|
108
|
+
|
|
109
|
+
Within each 300 row, all readings share the same ``interval_date``
|
|
110
|
+
and the parser emits them contiguously, so the rollup is a streaming
|
|
111
|
+
O(1)-memory operation: we accumulate while ``(channel, date)`` stays
|
|
112
|
+
constant and emit when it changes.
|
|
113
|
+
|
|
114
|
+
Empty cells in the source are coerced to ``0.0`` upstream (see
|
|
115
|
+
:class:`IntervalReading`); they are summed as zero.
|
|
116
|
+
"""
|
|
117
|
+
current_key: Optional[Tuple[ChannelKey, datetime]] = None
|
|
118
|
+
current_uom: str = ""
|
|
119
|
+
total = 0.0
|
|
120
|
+
count = 0
|
|
121
|
+
flags: set[str] = set()
|
|
122
|
+
|
|
123
|
+
for r in readings:
|
|
124
|
+
key = (ChannelKey(r.nmi, r.register_id, r.nmi_suffix), r.interval_date)
|
|
125
|
+
if current_key is None:
|
|
126
|
+
current_key = key
|
|
127
|
+
current_uom = r.uom
|
|
128
|
+
elif key != current_key:
|
|
129
|
+
yield DailyTotal(
|
|
130
|
+
nmi=current_key[0].nmi,
|
|
131
|
+
register_id=current_key[0].register_id,
|
|
132
|
+
nmi_suffix=current_key[0].nmi_suffix,
|
|
133
|
+
uom=current_uom,
|
|
134
|
+
interval_date=current_key[1],
|
|
135
|
+
total=total,
|
|
136
|
+
interval_count=count,
|
|
137
|
+
unique_quality_flags=frozenset(flags),
|
|
138
|
+
)
|
|
139
|
+
current_key = key
|
|
140
|
+
current_uom = r.uom
|
|
141
|
+
total = 0.0
|
|
142
|
+
count = 0
|
|
143
|
+
flags = set()
|
|
144
|
+
total += r.value
|
|
145
|
+
count += 1
|
|
146
|
+
if r.quality_method:
|
|
147
|
+
flags.add(r.quality_method[:1])
|
|
148
|
+
|
|
149
|
+
if current_key is not None:
|
|
150
|
+
yield DailyTotal(
|
|
151
|
+
nmi=current_key[0].nmi,
|
|
152
|
+
register_id=current_key[0].register_id,
|
|
153
|
+
nmi_suffix=current_key[0].nmi_suffix,
|
|
154
|
+
uom=current_uom,
|
|
155
|
+
interval_date=current_key[1],
|
|
156
|
+
total=total,
|
|
157
|
+
interval_count=count,
|
|
158
|
+
unique_quality_flags=frozenset(flags),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
__all__ = [
|
|
163
|
+
"ChannelKey",
|
|
164
|
+
"DailyTotal",
|
|
165
|
+
"daily_totals",
|
|
166
|
+
"group_by_nmi",
|
|
167
|
+
"iter_chunks",
|
|
168
|
+
]
|
aemo_mdff_reader/cli.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Command-line interface for aemo-mdff-reader.
|
|
2
|
+
|
|
3
|
+
Installed as the ``aemo-mdff-reader`` console script via the project's
|
|
4
|
+
``[project.scripts]`` entry in ``pyproject.toml``.
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
aemo-mdff-reader INPUT [-o OUTPUT] [--records intervals|accumulations]
|
|
9
|
+
[--format csv|parquet]
|
|
10
|
+
[--nmi NMI [--nmi NMI ...]]
|
|
11
|
+
[--start YYYY-MM-DD] [--end YYYY-MM-DD]
|
|
12
|
+
[--validate]
|
|
13
|
+
|
|
14
|
+
If ``OUTPUT`` is omitted, results are written to stdout.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import sys
|
|
21
|
+
from datetime import date, datetime
|
|
22
|
+
from typing import Iterable, Iterator, Optional, Sequence
|
|
23
|
+
|
|
24
|
+
from . import __version__
|
|
25
|
+
from .parser import (
|
|
26
|
+
parse,
|
|
27
|
+
parse_accumulations,
|
|
28
|
+
parse_accumulations_to_columns,
|
|
29
|
+
parse_to_columns,
|
|
30
|
+
validate_file,
|
|
31
|
+
write_accumulations_csv,
|
|
32
|
+
write_csv,
|
|
33
|
+
)
|
|
34
|
+
from .types import AccumulationReading, IntervalReading
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _iso_date(value: str) -> date:
|
|
38
|
+
"""Parse a YYYY-MM-DD argparse value, raising ArgumentTypeError on bad input."""
|
|
39
|
+
try:
|
|
40
|
+
return datetime.strptime(value, "%Y-%m-%d").date()
|
|
41
|
+
except ValueError as exc:
|
|
42
|
+
raise argparse.ArgumentTypeError(f"expected YYYY-MM-DD date, got {value!r}") from exc
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
46
|
+
p = argparse.ArgumentParser(
|
|
47
|
+
prog="aemo-mdff-reader",
|
|
48
|
+
description=(
|
|
49
|
+
"Read an AEMO NEM12 / NEM13 file and emit a flat CSV. Streams in O(1) memory."
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
p.add_argument("input", help="Path to the NEM12 / NEM13 input file (CSV).")
|
|
53
|
+
p.add_argument(
|
|
54
|
+
"-o",
|
|
55
|
+
"--output",
|
|
56
|
+
default="-",
|
|
57
|
+
help="Output path. Use '-' (the default) for stdout.",
|
|
58
|
+
)
|
|
59
|
+
p.add_argument(
|
|
60
|
+
"--records",
|
|
61
|
+
choices=("intervals", "accumulations"),
|
|
62
|
+
default="intervals",
|
|
63
|
+
help=(
|
|
64
|
+
"Which record type to emit. 'intervals' = NEM12 300 records "
|
|
65
|
+
"(default), 'accumulations' = NEM13 250 records."
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
p.add_argument(
|
|
69
|
+
"--format",
|
|
70
|
+
choices=("csv", "parquet"),
|
|
71
|
+
default="csv",
|
|
72
|
+
help="Output format. 'parquet' requires pandas + pyarrow.",
|
|
73
|
+
)
|
|
74
|
+
p.add_argument(
|
|
75
|
+
"--nmi",
|
|
76
|
+
action="append",
|
|
77
|
+
default=None,
|
|
78
|
+
metavar="NMI",
|
|
79
|
+
help=(
|
|
80
|
+
"Restrict output to the given NMI. Repeat the flag to keep "
|
|
81
|
+
"more than one (e.g. --nmi A --nmi B). Default: all NMIs."
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
p.add_argument(
|
|
85
|
+
"--start",
|
|
86
|
+
type=_iso_date,
|
|
87
|
+
default=None,
|
|
88
|
+
metavar="YYYY-MM-DD",
|
|
89
|
+
help="Drop rows whose interval / current-read date is before this date.",
|
|
90
|
+
)
|
|
91
|
+
p.add_argument(
|
|
92
|
+
"--end",
|
|
93
|
+
type=_iso_date,
|
|
94
|
+
default=None,
|
|
95
|
+
metavar="YYYY-MM-DD",
|
|
96
|
+
help="Drop rows whose interval / current-read date is after this date.",
|
|
97
|
+
)
|
|
98
|
+
p.add_argument(
|
|
99
|
+
"--validate",
|
|
100
|
+
action="store_true",
|
|
101
|
+
help=(
|
|
102
|
+
"Run structural validation against the AEMO MDFF spec and exit. "
|
|
103
|
+
"Returns 0 if the file is valid, 1 if issues were found."
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
p.add_argument(
|
|
107
|
+
"--version",
|
|
108
|
+
action="version",
|
|
109
|
+
version=f"aemo-mdff-reader {__version__}",
|
|
110
|
+
)
|
|
111
|
+
return p
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _filter_intervals(
|
|
115
|
+
readings: Iterable[IntervalReading],
|
|
116
|
+
*,
|
|
117
|
+
nmis: Optional[Sequence[str]],
|
|
118
|
+
start: Optional[date],
|
|
119
|
+
end: Optional[date],
|
|
120
|
+
) -> Iterator[IntervalReading]:
|
|
121
|
+
nmi_set = set(nmis) if nmis else None
|
|
122
|
+
for r in readings:
|
|
123
|
+
if nmi_set is not None and r.nmi not in nmi_set:
|
|
124
|
+
continue
|
|
125
|
+
d = r.interval_date.date()
|
|
126
|
+
if start is not None and d < start:
|
|
127
|
+
continue
|
|
128
|
+
if end is not None and d > end:
|
|
129
|
+
continue
|
|
130
|
+
yield r
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _filter_accumulations(
|
|
134
|
+
readings: Iterable[AccumulationReading],
|
|
135
|
+
*,
|
|
136
|
+
nmis: Optional[Sequence[str]],
|
|
137
|
+
start: Optional[date],
|
|
138
|
+
end: Optional[date],
|
|
139
|
+
) -> Iterator[AccumulationReading]:
|
|
140
|
+
nmi_set = set(nmis) if nmis else None
|
|
141
|
+
for r in readings:
|
|
142
|
+
if nmi_set is not None and r.nmi not in nmi_set:
|
|
143
|
+
continue
|
|
144
|
+
if r.current_register_read_datetime is not None:
|
|
145
|
+
d = r.current_register_read_datetime.date()
|
|
146
|
+
if start is not None and d < start:
|
|
147
|
+
continue
|
|
148
|
+
if end is not None and d > end:
|
|
149
|
+
continue
|
|
150
|
+
yield r
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _emit_parquet(args: argparse.Namespace) -> int:
|
|
154
|
+
try:
|
|
155
|
+
import pandas as pd
|
|
156
|
+
except ImportError:
|
|
157
|
+
print(
|
|
158
|
+
"parquet output requires pandas + pyarrow: pip install aemo-mdff-reader[parquet]",
|
|
159
|
+
file=sys.stderr,
|
|
160
|
+
)
|
|
161
|
+
return 2
|
|
162
|
+
if args.records == "accumulations":
|
|
163
|
+
cols = parse_accumulations_to_columns(args.input)
|
|
164
|
+
df = pd.DataFrame(cols)
|
|
165
|
+
else:
|
|
166
|
+
cols = parse_to_columns(args.input)
|
|
167
|
+
df = pd.DataFrame(cols)
|
|
168
|
+
|
|
169
|
+
# Apply --nmi / --start / --end filters in-DataFrame so the parquet
|
|
170
|
+
# output matches what the streaming CSV path would emit.
|
|
171
|
+
if args.nmi:
|
|
172
|
+
df = df[df["NMI"].isin(args.nmi)]
|
|
173
|
+
if args.records == "accumulations":
|
|
174
|
+
date_col = "CurrentRegisterReadDatetime"
|
|
175
|
+
else:
|
|
176
|
+
date_col = "IntervalDate"
|
|
177
|
+
if args.start is not None:
|
|
178
|
+
df = df[df[date_col] >= pd.Timestamp(args.start)]
|
|
179
|
+
if args.end is not None:
|
|
180
|
+
df = df[df[date_col] <= pd.Timestamp(args.end)]
|
|
181
|
+
|
|
182
|
+
df.to_parquet(args.output)
|
|
183
|
+
return 0
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
187
|
+
args = _build_parser().parse_args(argv)
|
|
188
|
+
|
|
189
|
+
if args.validate:
|
|
190
|
+
issues = validate_file(args.input)
|
|
191
|
+
if issues:
|
|
192
|
+
for msg in issues:
|
|
193
|
+
print(msg, file=sys.stderr)
|
|
194
|
+
return 1
|
|
195
|
+
print("OK", file=sys.stderr)
|
|
196
|
+
return 0
|
|
197
|
+
|
|
198
|
+
if args.format == "parquet":
|
|
199
|
+
return _emit_parquet(args)
|
|
200
|
+
|
|
201
|
+
out_target = sys.stdout if args.output == "-" else args.output
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
if args.records == "accumulations":
|
|
205
|
+
acc_stream = _filter_accumulations(
|
|
206
|
+
parse_accumulations(args.input),
|
|
207
|
+
nmis=args.nmi,
|
|
208
|
+
start=args.start,
|
|
209
|
+
end=args.end,
|
|
210
|
+
)
|
|
211
|
+
write_accumulations_csv(acc_stream, out_target)
|
|
212
|
+
else:
|
|
213
|
+
int_stream = _filter_intervals(
|
|
214
|
+
parse(args.input),
|
|
215
|
+
nmis=args.nmi,
|
|
216
|
+
start=args.start,
|
|
217
|
+
end=args.end,
|
|
218
|
+
)
|
|
219
|
+
write_csv(int_stream, out_target)
|
|
220
|
+
except BrokenPipeError:
|
|
221
|
+
# Downstream consumer (e.g. ``head``) closed the pipe early —
|
|
222
|
+
# this is normal, not an error.
|
|
223
|
+
try:
|
|
224
|
+
sys.stdout.flush()
|
|
225
|
+
except BrokenPipeError:
|
|
226
|
+
pass
|
|
227
|
+
return 0
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
if __name__ == "__main__": # pragma: no cover
|
|
231
|
+
sys.exit(main())
|