aemo-mdff-reader 2.2.1__tar.gz → 4.0.1__tar.gz
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-2.2.1 → aemo_mdff_reader-4.0.1}/CHANGELOG.md +74 -0
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/CONTRIBUTING.md +6 -6
- {aemo_mdff_reader-2.2.1/aemo_mdff_reader.egg-info → aemo_mdff_reader-4.0.1}/PKG-INFO +19 -15
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/README.md +17 -10
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader/__init__.py +1 -1
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader/aggregate.py +22 -9
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader/parser.py +153 -37
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader/spec.py +2 -2
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader/types.py +11 -9
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1/aemo_mdff_reader.egg-info}/PKG-INFO +19 -15
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader.egg-info/SOURCES.txt +0 -4
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader.egg-info/requires.txt +0 -3
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/pyproject.toml +7 -18
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/test_edge_cases.py +44 -3
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/test_ergonomics.py +14 -0
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/test_events_b2b.py +50 -0
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/test_nem13.py +36 -0
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/test_parser.py +96 -0
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/test_spec.py +12 -0
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/test_validation.py +37 -0
- aemo_mdff_reader-2.2.1/aemo_mdff_reader/sql/__init__.py +0 -28
- aemo_mdff_reader-2.2.1/aemo_mdff_reader/sql/query.py +0 -102
- aemo_mdff_reader-2.2.1/aemo_mdff_reader/sql/store.py +0 -186
- aemo_mdff_reader-2.2.1/tests/test_sql.py +0 -55
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/LICENSE +0 -0
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/MANIFEST.in +0 -0
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader/cli.py +0 -0
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader/py.typed +0 -0
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader/reader.py +0 -0
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader.egg-info/dependency_links.txt +0 -0
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader.egg-info/entry_points.txt +0 -0
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader.egg-info/top_level.txt +0 -0
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/benchmarks/bench_parser.py +0 -0
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/setup.cfg +0 -0
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/setup.py +0 -0
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/__init__.py +0 -0
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/fixtures/generate.py +0 -0
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/fixtures/sample_nem12.csv +0 -0
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/fixtures/sample_nem13.csv +0 -0
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/test_cli.py +0 -0
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/test_datetime_formats.py +0 -0
- {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/test_large_files.py +0 -0
|
@@ -4,6 +4,80 @@ All notable changes are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the
|
|
5
5
|
project follows [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [4.0.1](https://github.com/Utilified/aemo-mdff-reader/compare/v4.0.0...v4.0.1) (2026-11-02)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Bug Fixes
|
|
11
|
+
|
|
12
|
+
* **deps:** bump certifi to 2026.10.19 for PYSEC-2026-4320 ([#145](https://github.com/Utilified/aemo-mdff-reader/issues/145)) ([7b66f2a](https://github.com/Utilified/aemo-mdff-reader/commit/7b66f2a4cd2b5e333c5258dcfd41277909c9ac62))
|
|
13
|
+
* **deps:** bump msgpack to 1.2.4 for CVE-2026-114613 ([#148](https://github.com/Utilified/aemo-mdff-reader/issues/148)) ([06eb1ac](https://github.com/Utilified/aemo-mdff-reader/commit/06eb1ace6e8a94d4a87cad33211713b917a946e0))
|
|
14
|
+
|
|
15
|
+
## [4.0.0](https://github.com/Utilified/aemo-mdff-reader/compare/v3.0.1...v4.0.0) (2026-09-01)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### ⚠ BREAKING CHANGES
|
|
19
|
+
|
|
20
|
+
* interval and accumulation reading values are now `float | None`. An
|
|
21
|
+
empty cell parses to `None` (missing) instead of `0.0` — a missing
|
|
22
|
+
reading must not be indistinguishable from a genuine zero in a
|
|
23
|
+
billing input. This affects `IntervalReading.value`, the `Value`
|
|
24
|
+
column in `to_columns` / `parse_to_columns` / the pandas and parquet
|
|
25
|
+
paths, and NEM13 `previous_register_read`, `current_register_read`
|
|
26
|
+
and `quantity`. `aggregate.daily_totals` skips `None` readings rather
|
|
27
|
+
than summing them as zero, excludes them from `interval_count`, and
|
|
28
|
+
reports them in the new `DailyTotal.missing_count` field.
|
|
29
|
+
* the `aemo_mdff_reader.sql` subpackage and the `[mysql]` extra are
|
|
30
|
+
removed, along with `QueryBuilder`, `COLUMNS`, `Storer` and
|
|
31
|
+
`DBCredentials`. The subpackage referenced `NEMReader` attributes that
|
|
32
|
+
have not existed since v2, so every entry point raised
|
|
33
|
+
`AttributeError` on call, and its query builder interpolated
|
|
34
|
+
file-derived values into SQL unescaped. There is no replacement:
|
|
35
|
+
persist via `to_columns()` / `iter_columns_chunks()` and your own
|
|
36
|
+
parameterised inserts.
|
|
37
|
+
* files that previously parsed silently now raise `NEM12ParseError`: a
|
|
38
|
+
300 row carrying more interval values than its parent 200 record's
|
|
39
|
+
IntervalLength allows, a non-numeric interval value or reason code, an
|
|
40
|
+
impossible date, and a 400 row whose StartInterval / EndInterval falls
|
|
41
|
+
outside the NMI's interval count. Trailing empty cells on a 300 row
|
|
42
|
+
(Excel padding) are tolerated and do not count against the field
|
|
43
|
+
bound.
|
|
44
|
+
|
|
45
|
+
### Bug Fixes
|
|
46
|
+
|
|
47
|
+
* raise on over-long 300 rows instead of silently truncating them. A
|
|
48
|
+
15-minute file under a stale 30-minute 200 header emitted 48 readings,
|
|
49
|
+
dropped the remaining 48, and took `quality_method` from a dropped
|
|
50
|
+
*value*. `validate_file` now checks 300 field counts too — it
|
|
51
|
+
previously returned clean for a 30-minute NMI carrying 288 values.
|
|
52
|
+
* wrap the remaining raw `ValueError`s at the parse boundary as
|
|
53
|
+
`NEM12ParseError` (`_parse_int`, `_parse_float`, the columnar value
|
|
54
|
+
path, the 400 record's interval bounds, and the fixed-width
|
|
55
|
+
`datetime()` construction), each naming the offending field and value.
|
|
56
|
+
* bound-check 400 StartInterval / EndInterval against the NMI's
|
|
57
|
+
intervals per day — `400,1,999,S` previously parsed against a
|
|
58
|
+
48-interval day.
|
|
59
|
+
* correct the `spec.DIRECTION_INDICATORS` descriptions, which had Import
|
|
60
|
+
and Export the wrong way round.
|
|
61
|
+
|
|
62
|
+
## [3.0.1](https://github.com/Utilified/aemo-mdff-reader/compare/v3.0.0...v3.0.1) (2026-07-18)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
### Bug Fixes
|
|
66
|
+
|
|
67
|
+
* **fuzz:** install with --ignore-requires-python on the 3.11 OSS-Fuzz image ([#75](https://github.com/Utilified/aemo-mdff-reader/issues/75)) ([28605fe](https://github.com/Utilified/aemo-mdff-reader/commit/28605fe6c408891656bf106d7facdab1afe7dd8e))
|
|
68
|
+
* tolerate unparseable 100-record DateTime instead of failing the file ([#73](https://github.com/Utilified/aemo-mdff-reader/issues/73)) ([ec84dd9](https://github.com/Utilified/aemo-mdff-reader/commit/ec84dd9a42c8c92445b0f6ef6fab7a1417ece220)), closes [#72](https://github.com/Utilified/aemo-mdff-reader/issues/72)
|
|
69
|
+
|
|
70
|
+
## [3.0.0](https://github.com/Utilified/aemo-mdff-reader/compare/v2.2.1...v3.0.0) (2026-07-16)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
### ⚠ BREAKING CHANGES
|
|
74
|
+
|
|
75
|
+
* minimum Python version is now 3.12. Users on 3.11 should pin to aemo-mdff-reader<2.3.
|
|
76
|
+
|
|
77
|
+
### Features
|
|
78
|
+
|
|
79
|
+
* drop Python 3.11 support ([#70](https://github.com/Utilified/aemo-mdff-reader/issues/70)) ([383ff6d](https://github.com/Utilified/aemo-mdff-reader/commit/383ff6de7430372a8a67bd1b1a8271aa849f53eb))
|
|
80
|
+
|
|
7
81
|
## [2.2.1](https://github.com/Utilified/aemo-mdff-reader/compare/v2.2.0...v2.2.1) (2026-05-10)
|
|
8
82
|
|
|
9
83
|
|
|
@@ -36,16 +36,16 @@ To regenerate manually after editing them or `pyproject.toml`:
|
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
38
|
pip install uv
|
|
39
|
-
uv pip compile --generate-hashes --strip-extras --python-version 3.
|
|
40
|
-
uv pip compile --generate-hashes --strip-extras --python-version 3.
|
|
41
|
-
uv pip compile --generate-hashes --strip-extras --python-version 3.
|
|
42
|
-
uv pip compile --generate-hashes --strip-extras --python-version 3.
|
|
39
|
+
uv pip compile --generate-hashes --strip-extras --python-version 3.12 --output-file requirements/dev.txt requirements/dev.in
|
|
40
|
+
uv pip compile --generate-hashes --strip-extras --python-version 3.12 --output-file requirements/lint.txt requirements/lint.in
|
|
41
|
+
uv pip compile --generate-hashes --strip-extras --python-version 3.12 --output-file requirements/build.txt requirements/build.in
|
|
42
|
+
uv pip compile --generate-hashes --strip-extras --python-version 3.12 --output-file requirements/audit.txt requirements/audit.in
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
`uv` is used over `pip-tools` because its resolver handles the
|
|
46
|
-
3.
|
|
46
|
+
3.12+ matrix without back-tracking failures, and it emits hashes
|
|
47
47
|
for every wheel of each pinned version (so a single lock file works
|
|
48
|
-
across the whole CI matrix). The `--python-version 3.
|
|
48
|
+
across the whole CI matrix). The `--python-version 3.12` flag pins
|
|
49
49
|
resolution to the lowest supported interpreter so packages whose
|
|
50
50
|
latest releases dropped support for it are correctly stepped back
|
|
51
51
|
to a compatible version.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aemo-mdff-reader
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.1
|
|
4
4
|
Summary: Fast streaming reader for AEMO MDFF NEM12 / NEM13 metering files. Zero required dependencies.
|
|
5
5
|
Author: Utilified Holdings Pty Ltd
|
|
6
6
|
License-Expression: MIT
|
|
@@ -15,13 +15,12 @@ Classifier: Intended Audience :: Information Technology
|
|
|
15
15
|
Classifier: Operating System :: OS Independent
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
17
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
20
|
Classifier: Topic :: Office/Business :: Financial
|
|
22
21
|
Classifier: Topic :: Scientific/Engineering
|
|
23
22
|
Classifier: Topic :: Utilities
|
|
24
|
-
Requires-Python: >=3.
|
|
23
|
+
Requires-Python: >=3.12
|
|
25
24
|
Description-Content-Type: text/markdown
|
|
26
25
|
License-File: LICENSE
|
|
27
26
|
Provides-Extra: pandas
|
|
@@ -29,8 +28,6 @@ Requires-Dist: pandas>=2; extra == "pandas"
|
|
|
29
28
|
Provides-Extra: parquet
|
|
30
29
|
Requires-Dist: pandas>=2; extra == "parquet"
|
|
31
30
|
Requires-Dist: pyarrow>=10; extra == "parquet"
|
|
32
|
-
Provides-Extra: mysql
|
|
33
|
-
Requires-Dist: PyMySQL>=1.0; extra == "mysql"
|
|
34
31
|
Provides-Extra: dev
|
|
35
32
|
Requires-Dist: pytest>=7; extra == "dev"
|
|
36
33
|
Requires-Dist: pytest-cov>=4; extra == "dev"
|
|
@@ -47,7 +44,7 @@ Dynamic: license-file
|
|
|
47
44
|
|
|
48
45
|
[](https://github.com/Utilified/aemo-mdff-reader/actions/workflows/ci.yml)
|
|
49
46
|
[](https://pypi.org/project/aemo-mdff-reader/)
|
|
50
|
-
[](https://pypi.org/project/aemo-mdff-reader/)
|
|
51
48
|
[](LICENSE)
|
|
52
49
|
[](https://github.com/Utilified/aemo-mdff-reader/actions/workflows/codeql.yml)
|
|
53
50
|
[](https://scorecard.dev/viewer/?uri=github.com/Utilified/aemo-mdff-reader)
|
|
@@ -56,7 +53,7 @@ Fast, zero-dependency streaming reader for AEMO **NEM12** and **NEM13**
|
|
|
56
53
|
metering files. Implements AEMO MDFF (Meter Data File Format) v2.6.
|
|
57
54
|
|
|
58
55
|
- O(1) memory — iterate through millions of intervals.
|
|
59
|
-
- Pure stdlib core; pandas /
|
|
56
|
+
- Pure stdlib core; pandas / pyarrow are opt-in extras.
|
|
60
57
|
- ~2 M readings/sec on the columnar fast path.
|
|
61
58
|
- Includes an `aemo-mdff-reader` CLI.
|
|
62
59
|
|
|
@@ -66,8 +63,8 @@ metering files. Implements AEMO MDFF (Meter Data File Format) v2.6.
|
|
|
66
63
|
pip install aemo-mdff-reader
|
|
67
64
|
|
|
68
65
|
# optional extras
|
|
69
|
-
pip install aemo-mdff-reader[pandas] # to_dataframe()
|
|
70
|
-
pip install aemo-mdff-reader[
|
|
66
|
+
pip install aemo-mdff-reader[pandas] # to_dataframe()
|
|
67
|
+
pip install aemo-mdff-reader[parquet] # parquet output (pandas + pyarrow)
|
|
71
68
|
```
|
|
72
69
|
|
|
73
70
|
## Use
|
|
@@ -84,8 +81,8 @@ Or as a flat CSV / DataFrame:
|
|
|
84
81
|
```python
|
|
85
82
|
from aemo_mdff_reader import parse, write_csv, to_dataframe
|
|
86
83
|
|
|
87
|
-
write_csv(parse("metering.csv"), "out.csv")
|
|
88
|
-
df = to_dataframe("metering.csv")
|
|
84
|
+
write_csv(parse("metering.csv"), "out.csv") # no pandas
|
|
85
|
+
df = to_dataframe("metering.csv") # needs [pandas]
|
|
89
86
|
```
|
|
90
87
|
|
|
91
88
|
From the command line:
|
|
@@ -104,7 +101,7 @@ Each parsed record is a slots-based class with named attributes plus a
|
|
|
104
101
|
|
|
105
102
|
```python
|
|
106
103
|
for r in parse("metering.csv"):
|
|
107
|
-
payload = r.to_dict()
|
|
104
|
+
payload = r.to_dict() # {"nmi": "...", "value": 0.12, ...}
|
|
108
105
|
print(r.quality_flag, r.method_flag) # split of the QMM field, e.g. "S", "52"
|
|
109
106
|
```
|
|
110
107
|
|
|
@@ -156,6 +153,13 @@ Each `parse(...)` yields an `IntervalReading` with `nmi`,
|
|
|
156
153
|
the type stubs (`from aemo_mdff_reader import IntervalReading`) for the
|
|
157
154
|
exact signatures.
|
|
158
155
|
|
|
156
|
+
`value` is `float | None`: an empty cell in the source file is a
|
|
157
|
+
*missing* reading and stays `None` rather than becoming `0.0`, which
|
|
158
|
+
would be indistinguishable from a genuine zero. The same holds for the
|
|
159
|
+
NEM13 `previous_register_read`, `current_register_read` and `quantity`
|
|
160
|
+
fields. `aggregate.daily_totals` skips missing readings and reports how
|
|
161
|
+
many it saw in `DailyTotal.missing_count`.
|
|
162
|
+
|
|
159
163
|
## Notes
|
|
160
164
|
|
|
161
165
|
- **Spec**: AEMO Meter Data File Format Specification NEM12 & NEM13,
|
|
@@ -166,8 +170,8 @@ exact signatures.
|
|
|
166
170
|
as constants in `aemo_mdff_reader.spec` for callers that want stricter
|
|
167
171
|
validation than the parser performs.
|
|
168
172
|
- **Tolerant**: UTF-8 BOM is consumed silently, LF and CRLF both work,
|
|
169
|
-
and empty interval cells
|
|
170
|
-
|
|
173
|
+
and empty interval cells parse to `None` (missing), never `0.0`.
|
|
174
|
+
Datetime fields accept the spec
|
|
171
175
|
forms (`YYYYMMDD`, `YYYYMMDDhhmmss`) and a few common non-spec
|
|
172
176
|
variants (`YYYY-MM-DD`, ISO `YYYY-MM-DDTHH:MM:SS`, with or without a
|
|
173
177
|
`Z` / `±HH:MM` / `±HHMM` timezone suffix — the suffix is stripped
|
|
@@ -237,7 +241,7 @@ pip install -e .[dev]
|
|
|
237
241
|
pytest
|
|
238
242
|
```
|
|
239
243
|
|
|
240
|
-
CI runs ruff, mypy --strict, the test matrix on Python 3.
|
|
244
|
+
CI runs ruff, mypy --strict, the test matrix on Python 3.12 /
|
|
241
245
|
Linux / macOS / Windows, `pip-audit`, `bandit`, CodeQL, OpenSSF
|
|
242
246
|
Scorecard, and a wheel-install smoke test.
|
|
243
247
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/Utilified/aemo-mdff-reader/actions/workflows/ci.yml)
|
|
4
4
|
[](https://pypi.org/project/aemo-mdff-reader/)
|
|
5
|
-
[](https://pypi.org/project/aemo-mdff-reader/)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://github.com/Utilified/aemo-mdff-reader/actions/workflows/codeql.yml)
|
|
8
8
|
[](https://scorecard.dev/viewer/?uri=github.com/Utilified/aemo-mdff-reader)
|
|
@@ -11,7 +11,7 @@ Fast, zero-dependency streaming reader for AEMO **NEM12** and **NEM13**
|
|
|
11
11
|
metering files. Implements AEMO MDFF (Meter Data File Format) v2.6.
|
|
12
12
|
|
|
13
13
|
- O(1) memory — iterate through millions of intervals.
|
|
14
|
-
- Pure stdlib core; pandas /
|
|
14
|
+
- Pure stdlib core; pandas / pyarrow are opt-in extras.
|
|
15
15
|
- ~2 M readings/sec on the columnar fast path.
|
|
16
16
|
- Includes an `aemo-mdff-reader` CLI.
|
|
17
17
|
|
|
@@ -21,8 +21,8 @@ metering files. Implements AEMO MDFF (Meter Data File Format) v2.6.
|
|
|
21
21
|
pip install aemo-mdff-reader
|
|
22
22
|
|
|
23
23
|
# optional extras
|
|
24
|
-
pip install aemo-mdff-reader[pandas] # to_dataframe()
|
|
25
|
-
pip install aemo-mdff-reader[
|
|
24
|
+
pip install aemo-mdff-reader[pandas] # to_dataframe()
|
|
25
|
+
pip install aemo-mdff-reader[parquet] # parquet output (pandas + pyarrow)
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
## Use
|
|
@@ -39,8 +39,8 @@ Or as a flat CSV / DataFrame:
|
|
|
39
39
|
```python
|
|
40
40
|
from aemo_mdff_reader import parse, write_csv, to_dataframe
|
|
41
41
|
|
|
42
|
-
write_csv(parse("metering.csv"), "out.csv")
|
|
43
|
-
df = to_dataframe("metering.csv")
|
|
42
|
+
write_csv(parse("metering.csv"), "out.csv") # no pandas
|
|
43
|
+
df = to_dataframe("metering.csv") # needs [pandas]
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
From the command line:
|
|
@@ -59,7 +59,7 @@ Each parsed record is a slots-based class with named attributes plus a
|
|
|
59
59
|
|
|
60
60
|
```python
|
|
61
61
|
for r in parse("metering.csv"):
|
|
62
|
-
payload = r.to_dict()
|
|
62
|
+
payload = r.to_dict() # {"nmi": "...", "value": 0.12, ...}
|
|
63
63
|
print(r.quality_flag, r.method_flag) # split of the QMM field, e.g. "S", "52"
|
|
64
64
|
```
|
|
65
65
|
|
|
@@ -111,6 +111,13 @@ Each `parse(...)` yields an `IntervalReading` with `nmi`,
|
|
|
111
111
|
the type stubs (`from aemo_mdff_reader import IntervalReading`) for the
|
|
112
112
|
exact signatures.
|
|
113
113
|
|
|
114
|
+
`value` is `float | None`: an empty cell in the source file is a
|
|
115
|
+
*missing* reading and stays `None` rather than becoming `0.0`, which
|
|
116
|
+
would be indistinguishable from a genuine zero. The same holds for the
|
|
117
|
+
NEM13 `previous_register_read`, `current_register_read` and `quantity`
|
|
118
|
+
fields. `aggregate.daily_totals` skips missing readings and reports how
|
|
119
|
+
many it saw in `DailyTotal.missing_count`.
|
|
120
|
+
|
|
114
121
|
## Notes
|
|
115
122
|
|
|
116
123
|
- **Spec**: AEMO Meter Data File Format Specification NEM12 & NEM13,
|
|
@@ -121,8 +128,8 @@ exact signatures.
|
|
|
121
128
|
as constants in `aemo_mdff_reader.spec` for callers that want stricter
|
|
122
129
|
validation than the parser performs.
|
|
123
130
|
- **Tolerant**: UTF-8 BOM is consumed silently, LF and CRLF both work,
|
|
124
|
-
and empty interval cells
|
|
125
|
-
|
|
131
|
+
and empty interval cells parse to `None` (missing), never `0.0`.
|
|
132
|
+
Datetime fields accept the spec
|
|
126
133
|
forms (`YYYYMMDD`, `YYYYMMDDhhmmss`) and a few common non-spec
|
|
127
134
|
variants (`YYYY-MM-DD`, ISO `YYYY-MM-DDTHH:MM:SS`, with or without a
|
|
128
135
|
`Z` / `±HH:MM` / `±HHMM` timezone suffix — the suffix is stripped
|
|
@@ -192,7 +199,7 @@ pip install -e .[dev]
|
|
|
192
199
|
pytest
|
|
193
200
|
```
|
|
194
201
|
|
|
195
|
-
CI runs ruff, mypy --strict, the test matrix on Python 3.
|
|
202
|
+
CI runs ruff, mypy --strict, the test matrix on Python 3.12 /
|
|
196
203
|
Linux / macOS / Windows, `pip-audit`, `bandit`, CodeQL, OpenSSF
|
|
197
204
|
Scorecard, and a wheel-install smoke test.
|
|
198
205
|
|
|
@@ -61,11 +61,15 @@ class DailyTotal(NamedTuple):
|
|
|
61
61
|
"""A single channel's total energy / quantity for a calendar day.
|
|
62
62
|
|
|
63
63
|
``total`` is the sum of ``IntervalReading.value`` for every reading
|
|
64
|
-
that fell on ``interval_date``. ``
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
``
|
|
68
|
-
|
|
64
|
+
that fell on ``interval_date``. Readings with a ``None`` value are
|
|
65
|
+
*missing*, not zero: they are excluded from ``total`` and from
|
|
66
|
+
``interval_count``, and counted in ``missing_count`` instead — so a
|
|
67
|
+
day with any ``missing_count > 0`` is an incomplete total, not a
|
|
68
|
+
lower one. ``interval_count`` is the number of readings summed
|
|
69
|
+
(useful for spotting partial days). ``unique_quality_flags`` is the
|
|
70
|
+
set of distinct 1-character quality flags seen — a single ``{"A"}``
|
|
71
|
+
means the day is fully Actual, anything else hints at substituted or
|
|
72
|
+
estimated data.
|
|
69
73
|
"""
|
|
70
74
|
|
|
71
75
|
nmi: str
|
|
@@ -76,6 +80,7 @@ class DailyTotal(NamedTuple):
|
|
|
76
80
|
total: float
|
|
77
81
|
interval_count: int
|
|
78
82
|
unique_quality_flags: frozenset[str]
|
|
83
|
+
missing_count: int
|
|
79
84
|
|
|
80
85
|
|
|
81
86
|
def group_by_nmi(
|
|
@@ -111,13 +116,15 @@ def daily_totals(readings: Iterable[IntervalReading]) -> Iterator[DailyTotal]:
|
|
|
111
116
|
O(1)-memory operation: we accumulate while ``(channel, date)`` stays
|
|
112
117
|
constant and emit when it changes.
|
|
113
118
|
|
|
114
|
-
Empty cells in the source
|
|
115
|
-
:class:`IntervalReading`)
|
|
119
|
+
Empty cells in the source parse to ``None`` (see
|
|
120
|
+
:class:`IntervalReading`). They are skipped rather than summed as
|
|
121
|
+
zero, and reported per day via ``DailyTotal.missing_count``.
|
|
116
122
|
"""
|
|
117
123
|
current_key: Optional[Tuple[ChannelKey, datetime]] = None
|
|
118
124
|
current_uom: str = ""
|
|
119
125
|
total = 0.0
|
|
120
126
|
count = 0
|
|
127
|
+
missing = 0
|
|
121
128
|
flags: set[str] = set()
|
|
122
129
|
|
|
123
130
|
for r in readings:
|
|
@@ -135,14 +142,19 @@ def daily_totals(readings: Iterable[IntervalReading]) -> Iterator[DailyTotal]:
|
|
|
135
142
|
total=total,
|
|
136
143
|
interval_count=count,
|
|
137
144
|
unique_quality_flags=frozenset(flags),
|
|
145
|
+
missing_count=missing,
|
|
138
146
|
)
|
|
139
147
|
current_key = key
|
|
140
148
|
current_uom = r.uom
|
|
141
149
|
total = 0.0
|
|
142
150
|
count = 0
|
|
151
|
+
missing = 0
|
|
143
152
|
flags = set()
|
|
144
|
-
|
|
145
|
-
|
|
153
|
+
if r.value is None:
|
|
154
|
+
missing += 1
|
|
155
|
+
else:
|
|
156
|
+
total += r.value
|
|
157
|
+
count += 1
|
|
146
158
|
if r.quality_method:
|
|
147
159
|
flags.add(r.quality_method[:1])
|
|
148
160
|
|
|
@@ -156,6 +168,7 @@ def daily_totals(readings: Iterable[IntervalReading]) -> Iterator[DailyTotal]:
|
|
|
156
168
|
total=total,
|
|
157
169
|
interval_count=count,
|
|
158
170
|
unique_quality_flags=frozenset(flags),
|
|
171
|
+
missing_count=missing,
|
|
159
172
|
)
|
|
160
173
|
|
|
161
174
|
|