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.
Files changed (42) hide show
  1. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/CHANGELOG.md +74 -0
  2. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/CONTRIBUTING.md +6 -6
  3. {aemo_mdff_reader-2.2.1/aemo_mdff_reader.egg-info → aemo_mdff_reader-4.0.1}/PKG-INFO +19 -15
  4. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/README.md +17 -10
  5. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader/__init__.py +1 -1
  6. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader/aggregate.py +22 -9
  7. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader/parser.py +153 -37
  8. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader/spec.py +2 -2
  9. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader/types.py +11 -9
  10. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1/aemo_mdff_reader.egg-info}/PKG-INFO +19 -15
  11. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader.egg-info/SOURCES.txt +0 -4
  12. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader.egg-info/requires.txt +0 -3
  13. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/pyproject.toml +7 -18
  14. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/test_edge_cases.py +44 -3
  15. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/test_ergonomics.py +14 -0
  16. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/test_events_b2b.py +50 -0
  17. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/test_nem13.py +36 -0
  18. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/test_parser.py +96 -0
  19. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/test_spec.py +12 -0
  20. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/test_validation.py +37 -0
  21. aemo_mdff_reader-2.2.1/aemo_mdff_reader/sql/__init__.py +0 -28
  22. aemo_mdff_reader-2.2.1/aemo_mdff_reader/sql/query.py +0 -102
  23. aemo_mdff_reader-2.2.1/aemo_mdff_reader/sql/store.py +0 -186
  24. aemo_mdff_reader-2.2.1/tests/test_sql.py +0 -55
  25. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/LICENSE +0 -0
  26. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/MANIFEST.in +0 -0
  27. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader/cli.py +0 -0
  28. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader/py.typed +0 -0
  29. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader/reader.py +0 -0
  30. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader.egg-info/dependency_links.txt +0 -0
  31. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader.egg-info/entry_points.txt +0 -0
  32. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/aemo_mdff_reader.egg-info/top_level.txt +0 -0
  33. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/benchmarks/bench_parser.py +0 -0
  34. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/setup.cfg +0 -0
  35. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/setup.py +0 -0
  36. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/__init__.py +0 -0
  37. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/fixtures/generate.py +0 -0
  38. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/fixtures/sample_nem12.csv +0 -0
  39. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/fixtures/sample_nem13.csv +0 -0
  40. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/test_cli.py +0 -0
  41. {aemo_mdff_reader-2.2.1 → aemo_mdff_reader-4.0.1}/tests/test_datetime_formats.py +0 -0
  42. {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.11 --output-file requirements/dev.txt requirements/dev.in
40
- uv pip compile --generate-hashes --strip-extras --python-version 3.11 --output-file requirements/lint.txt requirements/lint.in
41
- uv pip compile --generate-hashes --strip-extras --python-version 3.11 --output-file requirements/build.txt requirements/build.in
42
- uv pip compile --generate-hashes --strip-extras --python-version 3.11 --output-file requirements/audit.txt requirements/audit.in
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.11–3.12 matrix without back-tracking failures, and it emits hashes
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.11` flag pins
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: 2.2.1
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.11
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
  [![CI](https://github.com/Utilified/aemo-mdff-reader/actions/workflows/ci.yml/badge.svg)](https://github.com/Utilified/aemo-mdff-reader/actions/workflows/ci.yml)
49
46
  [![PyPI](https://img.shields.io/pypi/v/aemo-mdff-reader.svg)](https://pypi.org/project/aemo-mdff-reader/)
50
- [![Python versions](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-blue)](https://pypi.org/project/aemo-mdff-reader/)
47
+ [![Python versions](https://img.shields.io/badge/python-3.12%20%7C%203.13-blue)](https://pypi.org/project/aemo-mdff-reader/)
51
48
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
52
49
  [![CodeQL](https://github.com/Utilified/aemo-mdff-reader/actions/workflows/codeql.yml/badge.svg)](https://github.com/Utilified/aemo-mdff-reader/actions/workflows/codeql.yml)
53
50
  [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/Utilified/aemo-mdff-reader/badge)](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 / PyMySQL are opt-in extras.
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() / parquet
70
- pip install aemo-mdff-reader[mysql] # SQL persistence
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") # no pandas
88
- df = to_dataframe("metering.csv") # needs [pandas]
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() # {"nmi": "...", "value": 0.12, ...}
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 are coerced to `0.0` (use `quality_method`
170
- to distinguish missing from zero). Datetime fields accept the spec
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.11 → 3.12 /
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
  [![CI](https://github.com/Utilified/aemo-mdff-reader/actions/workflows/ci.yml/badge.svg)](https://github.com/Utilified/aemo-mdff-reader/actions/workflows/ci.yml)
4
4
  [![PyPI](https://img.shields.io/pypi/v/aemo-mdff-reader.svg)](https://pypi.org/project/aemo-mdff-reader/)
5
- [![Python versions](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-blue)](https://pypi.org/project/aemo-mdff-reader/)
5
+ [![Python versions](https://img.shields.io/badge/python-3.12%20%7C%203.13-blue)](https://pypi.org/project/aemo-mdff-reader/)
6
6
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
7
  [![CodeQL](https://github.com/Utilified/aemo-mdff-reader/actions/workflows/codeql.yml/badge.svg)](https://github.com/Utilified/aemo-mdff-reader/actions/workflows/codeql.yml)
8
8
  [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/Utilified/aemo-mdff-reader/badge)](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 / PyMySQL are opt-in extras.
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() / parquet
25
- pip install aemo-mdff-reader[mysql] # SQL persistence
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") # no pandas
43
- df = to_dataframe("metering.csv") # needs [pandas]
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() # {"nmi": "...", "value": 0.12, ...}
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 are coerced to `0.0` (use `quality_method`
125
- to distinguish missing from zero). Datetime fields accept the spec
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.11 → 3.12 /
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
 
@@ -48,7 +48,7 @@ from .types import (
48
48
  NMIDetails,
49
49
  )
50
50
 
51
- __version__ = "2.2.1" # x-release-please-version
51
+ __version__ = "4.0.1" # x-release-please-version
52
52
 
53
53
  __all__ = [
54
54
  "INTERVAL_DATA_OUTPUT_HEADERS",
@@ -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``. ``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.
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 are coerced to ``0.0`` upstream (see
115
- :class:`IntervalReading`); they are summed as zero.
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
- total += r.value
145
- count += 1
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