aemo-mdff-reader 2.0.2__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.0.2/CHANGELOG.md +182 -0
  2. aemo_mdff_reader-2.0.2/CONTRIBUTING.md +118 -0
  3. aemo_mdff_reader-2.0.2/LICENSE +21 -0
  4. aemo_mdff_reader-2.0.2/MANIFEST.in +8 -0
  5. aemo_mdff_reader-2.0.2/PKG-INFO +247 -0
  6. aemo_mdff_reader-2.0.2/README.md +200 -0
  7. aemo_mdff_reader-2.0.2/aemo_mdff_reader/__init__.py +84 -0
  8. aemo_mdff_reader-2.0.2/aemo_mdff_reader/aggregate.py +168 -0
  9. aemo_mdff_reader-2.0.2/aemo_mdff_reader/cli.py +231 -0
  10. aemo_mdff_reader-2.0.2/aemo_mdff_reader/parser.py +1241 -0
  11. aemo_mdff_reader-2.0.2/aemo_mdff_reader/py.typed +0 -0
  12. aemo_mdff_reader-2.0.2/aemo_mdff_reader/reader.py +107 -0
  13. aemo_mdff_reader-2.0.2/aemo_mdff_reader/spec.py +242 -0
  14. aemo_mdff_reader-2.0.2/aemo_mdff_reader/sql/__init__.py +22 -0
  15. aemo_mdff_reader-2.0.2/aemo_mdff_reader/sql/query.py +102 -0
  16. aemo_mdff_reader-2.0.2/aemo_mdff_reader/sql/store.py +178 -0
  17. aemo_mdff_reader-2.0.2/aemo_mdff_reader/types.py +437 -0
  18. aemo_mdff_reader-2.0.2/aemo_mdff_reader.egg-info/PKG-INFO +247 -0
  19. aemo_mdff_reader-2.0.2/aemo_mdff_reader.egg-info/SOURCES.txt +40 -0
  20. aemo_mdff_reader-2.0.2/aemo_mdff_reader.egg-info/dependency_links.txt +1 -0
  21. aemo_mdff_reader-2.0.2/aemo_mdff_reader.egg-info/entry_points.txt +2 -0
  22. aemo_mdff_reader-2.0.2/aemo_mdff_reader.egg-info/requires.txt +21 -0
  23. aemo_mdff_reader-2.0.2/aemo_mdff_reader.egg-info/top_level.txt +1 -0
  24. aemo_mdff_reader-2.0.2/benchmarks/bench_parser.py +109 -0
  25. aemo_mdff_reader-2.0.2/pyproject.toml +147 -0
  26. aemo_mdff_reader-2.0.2/setup.cfg +4 -0
  27. aemo_mdff_reader-2.0.2/setup.py +5 -0
  28. aemo_mdff_reader-2.0.2/tests/__init__.py +0 -0
  29. aemo_mdff_reader-2.0.2/tests/fixtures/generate.py +104 -0
  30. aemo_mdff_reader-2.0.2/tests/fixtures/sample_nem12.csv +8 -0
  31. aemo_mdff_reader-2.0.2/tests/fixtures/sample_nem13.csv +5 -0
  32. aemo_mdff_reader-2.0.2/tests/test_cli.py +96 -0
  33. aemo_mdff_reader-2.0.2/tests/test_datetime_formats.py +68 -0
  34. aemo_mdff_reader-2.0.2/tests/test_edge_cases.py +167 -0
  35. aemo_mdff_reader-2.0.2/tests/test_ergonomics.py +313 -0
  36. aemo_mdff_reader-2.0.2/tests/test_events_b2b.py +107 -0
  37. aemo_mdff_reader-2.0.2/tests/test_large_files.py +136 -0
  38. aemo_mdff_reader-2.0.2/tests/test_nem13.py +155 -0
  39. aemo_mdff_reader-2.0.2/tests/test_parser.py +235 -0
  40. aemo_mdff_reader-2.0.2/tests/test_spec.py +70 -0
  41. aemo_mdff_reader-2.0.2/tests/test_sql.py +55 -0
  42. aemo_mdff_reader-2.0.2/tests/test_validation.py +128 -0
@@ -0,0 +1,182 @@
1
+ # Changelog
2
+
3
+ All notable changes are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the
5
+ project follows [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [2.0.2] — 2026-05-10
10
+
11
+ First release published to PyPI via Trusted Publishing from the now-public
12
+ GitHub repository. No source or behavioural changes versus 2.0.1 — this
13
+ release packages the public-launch hardening described below.
14
+
15
+ ### Added
16
+
17
+ - `CODE_OF_CONDUCT.md` (Contributor Covenant v2.1 adoption notice).
18
+ - GitHub issue forms for bug reports and feature requests, plus a
19
+ pull request template.
20
+ - CodeQL workflow for static analysis on push / PR / schedule.
21
+ - OpenSSF Scorecard workflow for supply-chain posture reporting.
22
+
23
+ ### Changed
24
+
25
+ - GitHub Actions across all workflows pinned to commit SHAs (managed
26
+ by Dependabot) for supply-chain hardening.
27
+
28
+ ### Fixed
29
+
30
+ - GitHub org casing in repository URLs (`utilified` → `Utilified`).
31
+
32
+ ## [2.0.1] — 2026-05-10
33
+
34
+ The 2.0.0 tag was cut before the project was renamed to
35
+ `aemo-mdff-reader`. 2.0.1 is the first version published under the new
36
+ name end-to-end. Source semantics are unchanged — this release exists
37
+ to give consumers a single stable tag matching the
38
+ `aemo_mdff_reader` import path and the `aemo-mdff-reader` PyPI dist
39
+ + CLI command.
40
+
41
+ ### Changed
42
+
43
+ - **Project renamed end-to-end** to `aemo-mdff-reader`: GitHub repo
44
+ `Utilified/aemo-mdff-reader`, PyPI distribution `aemo-mdff-reader`,
45
+ Python import `aemo_mdff_reader`, CLI command `aemo-mdff-reader`.
46
+ v1 callers using `from nem12_reader import …` must update their
47
+ imports; any references to the `nem12-reader` CLI command must be
48
+ updated. GitHub redirects the previous repo URL so existing git
49
+ pins (`git@github.com:Utilified/nem12-reader.git`) keep resolving.
50
+
51
+ ### Tooling
52
+
53
+ - Bumped GitHub Actions used in `ci.yml` / `release.yml` to their
54
+ current major versions: `actions/setup-python@v6`,
55
+ `actions/upload-artifact@v7`, `actions/download-artifact@v8`,
56
+ `softprops/action-gh-release@v3` (Node 24 runtime),
57
+ `sigstore/gh-action-sigstore-python@v3.3.0`.
58
+ - The release workflow's PyPI publish step is now gated on
59
+ `vars.PYPI_PUBLISH_ENABLED == 'true'`, so tag pushes run cleanly
60
+ before Trusted Publishing is configured (build + sign succeed;
61
+ publish + GitHub-release jobs skip).
62
+ - Repository description, homepage, and topic tags configured for
63
+ PyPI / GitHub discoverability.
64
+
65
+ ## [2.0.0] — 2026-05-10
66
+
67
+ ### Highlights
68
+
69
+ - **Renamed to `aemo-mdff-reader` end-to-end.** v1 shipped under
70
+ `nem12-reader` (GitHub repo, PyPI distribution, Python import, and
71
+ CLI command). v2 unifies all four surfaces under
72
+ `aemo-mdff-reader` / `aemo_mdff_reader` — the AEMO spec name (MDFF,
73
+ Meter Data File Format) covers both NEM12 (interval) and NEM13
74
+ (accumulation) data and disambiguates the project from the
75
+ unrelated `nem-reader` package on PyPI. GitHub redirects the
76
+ previous repo URL so existing git pins
77
+ (`git@github.com:Utilified/nem12-reader.git`) continue to resolve.
78
+ Source consumers must update their imports
79
+ (`from nem12_reader import …` → `from aemo_mdff_reader import …`)
80
+ and any references to the `nem12-reader` CLI command.
81
+ - **Spec target: AEMO MDFF v2.6** (effective 29 September 2024).
82
+ Previous v1.x of this package targeted MDFF v1.02 (2017). All
83
+ record-type schemas, allowed values, and reason-code descriptions
84
+ are aligned to v2.6 — including the v2.6 reason codes added by the
85
+ ICF_054 substitution-type review (codes 100–109) and the v2.6
86
+ designation of obsolete reason codes (Appendix F). The full set is
87
+ exposed as constants in the new `aemo_mdff_reader.spec` module.
88
+ - **Zero required dependencies**: pandas, PyMySQL, wrapt, six, tomli,
89
+ pytz, python-dateutil are no longer required. pandas / PyMySQL are
90
+ available as opt-in extras (`pip install aemo-mdff-reader[pandas]`,
91
+ `[mysql]`).
92
+ - **Streaming parser** (`aemo_mdff_reader.parse`): O(1) memory, works on
93
+ arbitrarily large files.
94
+ - **Columnar fast path** (`parse_to_columns`, `to_dataframe(path)`):
95
+ ~7× faster than the v1 `read_from_file` + `to_dataframe` flow.
96
+ - **Full NEM13 (250) accumulation support**: `parse_accumulations`,
97
+ `to_accumulations_dataframe`, `write_accumulations_csv`. `parse_all`
98
+ emits both NEM12 and NEM13 records in file order.
99
+ - **Full record-type coverage**: `parse_events()` (400 quality/event
100
+ rows), `parse_b2b()` (500 NEM12 + 550 NEM13 transaction details).
101
+ - **Spec-conformance utilities**: `nmi_checksum()`, `validate_nmi()`,
102
+ `validate_file()` plus a `aemo-mdff-reader --validate` CLI flag.
103
+ - **CLI** (`aemo-mdff-reader`): convert NEM12 / NEM13 → flat CSV / parquet
104
+ from the command line. Was previously documented but not implemented.
105
+ `--records intervals|accumulations` selects the record type;
106
+ `--validate` runs structural checks against the AEMO MDFF spec.
107
+
108
+ ### Added
109
+
110
+ - `aemo_mdff_reader.parse(source)` — generator-based streaming parser
111
+ (NEM12 300 records).
112
+ - `aemo_mdff_reader.parse_to_columns(source)` — single-pass columnar
113
+ parse without per-cell row-object allocation.
114
+ - `aemo_mdff_reader.parse_header(source)` — header-only fast read.
115
+ - `aemo_mdff_reader.parse_accumulations(source)` — NEM13 250 record
116
+ streaming parser.
117
+ - `aemo_mdff_reader.parse_accumulations_to_columns(source)` — columnar
118
+ NEM13 fast path.
119
+ - `aemo_mdff_reader.parse_all(source)` — unified iterator yielding both
120
+ 300 and 250 records in file order.
121
+ - `aemo_mdff_reader.to_dataframe(source)` and
122
+ `to_accumulations_dataframe(source)` — pandas DataFrame builders.
123
+ - `aemo_mdff_reader.write_csv(...)` and `write_accumulations_csv(...)` —
124
+ flat-CSV writers that work without pandas.
125
+ - `aemo_mdff_reader.types` — slots-based `Header`, `NMIDetails`,
126
+ `IntervalReading`, `IntervalEvent`, `AccumulationReading` classes.
127
+ - `aemo_mdff_reader.cli.main` — `aemo-mdff-reader` console script with
128
+ `--records intervals|accumulations` and `--format csv|parquet`.
129
+ - `pyproject.toml` (PEP 621) with full metadata, classifiers, and
130
+ optional extras (`pandas`, `parquet`, `mysql`, `dev`).
131
+ - `mypy --strict` configuration; the public `aemo_mdff_reader` surface
132
+ is fully typed and passes `mypy --strict`.
133
+ - `ruff` lint + format configuration.
134
+ - `LICENSE` file (MIT).
135
+ - `tests/` — 55-test pytest suite covering parser, edge cases,
136
+ datetime variants, CLI, NEM13, and lazy SQL imports.
137
+ - `benchmarks/bench_parser.py` — repeatable performance benchmark.
138
+ - GitHub Actions:
139
+ - CI matrix (Python 3.8 → 3.12, Linux/macOS/Windows).
140
+ - `ruff` lint job.
141
+ - `mypy --strict` job.
142
+ - Build job with `twine check --strict` and wheel-install smoke.
143
+ - Release workflow (`release.yml`) — publishes to PyPI on tag via
144
+ Trusted Publishing.
145
+ - `dependabot.yml` — weekly bumps for GitHub Actions and pip.
146
+ - `.pre-commit-config.yaml` — local lint + format + mypy hooks.
147
+
148
+ ### Changed
149
+
150
+ - `NEMReader().to_dataframe()` now uses the columnar fast path when
151
+ the source filename is known. Output schema clarifies interval
152
+ start/end timestamps and adds an explicit Suffix column.
153
+ - `aemo_mdff_reader.sql` no longer eagerly imports `pymysql`; the
154
+ dependency is only loaded when `Storer` is instantiated.
155
+
156
+ ### Removed
157
+
158
+ - `aemo_mdff_reader.nemstructure` package (Header / NMIData / IntervalData
159
+ abstractions and the dynamically-built `NEMField` schema). The new
160
+ `aemo_mdff_reader.types` module replaces this surface with simpler,
161
+ faster slots classes.
162
+ - Broken `aemo_mdff_reader.nemstructure.nem13` and `enem12` modules,
163
+ which called `Record.__init__(...)` with arguments the base class
164
+ never accepted.
165
+ - Dead `requirements.txt` pins for `numpy`, `wrapt`, `six`, `tomli`,
166
+ `python-dateutil`, `pytz`. None were actually used in the codebase.
167
+ - The unsanitized `test.csv` sample (contained a real NMI / meter
168
+ serial). Replaced with a synthesized fixture under
169
+ `tests/fixtures/sample_nem12.csv` and a generator script.
170
+
171
+ ### Fixed
172
+
173
+ - Quadratic-time row consumption in the legacy parser
174
+ (`array = array[1:]` in a hot loop) that scaled poorly on
175
+ large files.
176
+ - Per-`IntervalData`-row construction of ~289 `NEMField` objects
177
+ for 5-minute reads (eliminated entirely; schema is fixed in code).
178
+ - Eager pandas import on package load.
179
+
180
+ ## [1.0.1] — 2024
181
+
182
+ Initial public versions. See git history for detailed changes.
@@ -0,0 +1,118 @@
1
+ # Contributing
2
+
3
+ Thanks for your interest in improving aemo-mdff-reader. The repository
4
+ is small and the bar for contributions is straightforward: a pull
5
+ request that passes CI and doesn't regress the public benchmark is
6
+ welcome.
7
+
8
+ Naming: everything (GitHub repo `Utilified/aemo-mdff-reader`, PyPI
9
+ distribution `aemo-mdff-reader`, Python import `aemo_mdff_reader`,
10
+ CLI `aemo-mdff-reader`) is consistently named after the AEMO spec —
11
+ MDFF, the Meter Data File Format that covers both NEM12 (interval) and
12
+ NEM13 (accumulation) formats. GitHub serves redirects from the
13
+ previous `Utilified/nem12-reader` repo URL so existing git pins
14
+ continue to work; v1 callers importing `from nem12_reader …` need to
15
+ update to `from aemo_mdff_reader …`.
16
+
17
+ ## Setup
18
+
19
+ ```bash
20
+ git clone https://github.com/Utilified/aemo-mdff-reader.git
21
+ cd aemo-mdff-reader
22
+ python -m venv .venv && source .venv/bin/activate
23
+ pip install -e .[dev]
24
+ pytest
25
+ ```
26
+
27
+ ## Running the test suite
28
+
29
+ ```bash
30
+ pytest -v
31
+ pytest --cov=aemo_mdff_reader --cov-report=term-missing
32
+ ```
33
+
34
+ A small, sanitized NEM12 sample lives at
35
+ `tests/fixtures/sample_nem12.csv`. To generate larger synthetic files
36
+ for ad-hoc testing or benchmarking:
37
+
38
+ ```bash
39
+ python tests/fixtures/generate.py /tmp/big.csv --nmis 4 --days 365 --interval-minutes 5
40
+ ```
41
+
42
+ ## Performance
43
+
44
+ `benchmarks/bench_parser.py` is the canonical performance test. Please
45
+ run it before and after changes that touch the hot path
46
+ (`parser.py::_emit_intervals`, `parse_to_columns`, `to_columns`):
47
+
48
+ ```bash
49
+ python benchmarks/bench_parser.py --nmis 4 --days 365 --interval-minutes 5
50
+ ```
51
+
52
+ The streaming and columnar fast paths must remain at or above the
53
+ current published throughput.
54
+
55
+ ## Code style
56
+
57
+ - Pure stdlib in the core. **No** new required runtime dependencies.
58
+ Optional features go behind `[project.optional-dependencies]`.
59
+ - Type hints on all public symbols.
60
+ - Keep `aemo_mdff_reader.types` slots-based — per-row allocation is on
61
+ the hot path.
62
+
63
+ ## Reporting issues
64
+
65
+ Please open an issue with:
66
+
67
+ 1. The Python version and OS.
68
+ 2. A minimal reproducer (a few NEM12 rows is usually enough).
69
+ 3. The expected vs actual behaviour.
70
+
71
+ If you can share a sanitized sample file (no real NMIs / meter
72
+ serials), please attach it to the issue.
73
+
74
+ ## Releasing
75
+
76
+ Maintainers only. Releases are tagged and pushed to GitHub; the
77
+ `Release` workflow then builds, signs (sigstore), publishes to PyPI
78
+ (via Trusted Publishing), and attaches signatures to the GitHub Release.
79
+
80
+ ### One-time setup (per project)
81
+
82
+ Before the first release, configure these on PyPI / GitHub:
83
+
84
+ 1. **PyPI Trusted Publishing**: register the project at
85
+ https://pypi.org/manage/account/publishing/ with:
86
+ - PyPI project name: `aemo-mdff-reader`
87
+ - Owner: `Utilified`
88
+ - Repository: `aemo-mdff-reader`
89
+ - Workflow filename: `release.yml`
90
+ - Environment name: `pypi`
91
+ 2. **GitHub Environment**: in *Settings → Environments*, create an
92
+ environment named `pypi`. Add required reviewers if you want
93
+ manual approval before publish (recommended for the first few
94
+ releases). Without this environment, the publish job will fail with
95
+ a confusing "environment not found" error.
96
+
97
+ ### Cutting a release
98
+
99
+ ```bash
100
+ # Bump version in pyproject.toml AND aemo_mdff_reader/__init__.py.
101
+ # Update CHANGELOG.md with the new section.
102
+ git commit -am "release: vX.Y.Z"
103
+ git tag vX.Y.Z
104
+ git push origin main vX.Y.Z
105
+ ```
106
+
107
+ The `Release` workflow takes over from here. Verify the published
108
+ artefact at https://pypi.org/project/aemo-mdff-reader/ and the
109
+ GitHub Release at https://github.com/Utilified/aemo-mdff-reader/releases.
110
+
111
+ ### Local dry run
112
+
113
+ ```bash
114
+ python -m build
115
+ twine check --strict dist/*
116
+ python -m venv /tmp/smoke && /tmp/smoke/bin/pip install dist/*.whl
117
+ /tmp/smoke/bin/aemo-mdff-reader --version
118
+ ```
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023-2026 Utilified Holdings Pty Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,8 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include CONTRIBUTING.md
5
+ include pyproject.toml
6
+ include aemo_mdff_reader/py.typed
7
+ recursive-include tests *.py *.csv
8
+ recursive-include benchmarks *.py
@@ -0,0 +1,247 @@
1
+ Metadata-Version: 2.4
2
+ Name: aemo-mdff-reader
3
+ Version: 2.0.2
4
+ Summary: Fast streaming reader for AEMO MDFF NEM12 / NEM13 metering files. Zero required dependencies.
5
+ Author: Utilified Holdings Pty Ltd
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Utilified/aemo-mdff-reader
8
+ Project-URL: Repository, https://github.com/Utilified/aemo-mdff-reader
9
+ Project-URL: Issues, https://github.com/Utilified/aemo-mdff-reader/issues
10
+ Project-URL: Changelog, https://github.com/Utilified/aemo-mdff-reader/blob/main/CHANGELOG.md
11
+ Keywords: nem12,nem13,aemo,energy,metering,australia,mdff
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Information Technology
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Office/Business :: Financial
24
+ Classifier: Topic :: Scientific/Engineering
25
+ Classifier: Topic :: Utilities
26
+ Requires-Python: >=3.9
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Provides-Extra: pandas
30
+ Requires-Dist: pandas>=1.3; extra == "pandas"
31
+ Provides-Extra: parquet
32
+ Requires-Dist: pandas>=1.3; extra == "parquet"
33
+ Requires-Dist: pyarrow>=10; extra == "parquet"
34
+ Provides-Extra: mysql
35
+ Requires-Dist: PyMySQL>=1.0; extra == "mysql"
36
+ Provides-Extra: dev
37
+ Requires-Dist: pytest>=7; extra == "dev"
38
+ Requires-Dist: pytest-cov>=4; extra == "dev"
39
+ Requires-Dist: pandas>=1.3; extra == "dev"
40
+ Requires-Dist: pyarrow>=10; extra == "dev"
41
+ Requires-Dist: mypy>=1.8; extra == "dev"
42
+ Requires-Dist: ruff>=0.5; extra == "dev"
43
+ Requires-Dist: build>=1.0; extra == "dev"
44
+ Requires-Dist: twine>=5.0; extra == "dev"
45
+ Requires-Dist: packaging>=24.2; extra == "dev"
46
+ Dynamic: license-file
47
+
48
+ # aemo-mdff-reader
49
+
50
+ [![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)
51
+ [![PyPI](https://img.shields.io/pypi/v/aemo-mdff-reader.svg)](https://pypi.org/project/aemo-mdff-reader/)
52
+ [![Python versions](https://img.shields.io/pypi/pyversions/aemo-mdff-reader.svg)](https://pypi.org/project/aemo-mdff-reader/)
53
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
54
+
55
+ Fast, zero-dependency streaming reader for AEMO **NEM12** and **NEM13**
56
+ metering files. Implements AEMO MDFF (Meter Data File Format) v2.6.
57
+
58
+ - O(1) memory — iterate through millions of intervals.
59
+ - Pure stdlib core; pandas / PyMySQL are opt-in extras.
60
+ - ~2 M readings/sec on the columnar fast path.
61
+ - Includes an `aemo-mdff-reader` CLI.
62
+
63
+ ## Install
64
+
65
+ ```bash
66
+ pip install aemo-mdff-reader
67
+
68
+ # optional extras
69
+ pip install aemo-mdff-reader[pandas] # to_dataframe() / parquet
70
+ pip install aemo-mdff-reader[mysql] # SQL persistence
71
+ ```
72
+
73
+ ## Use
74
+
75
+ ```python
76
+ from aemo_mdff_reader import parse
77
+
78
+ for r in parse("metering.csv"):
79
+ print(r.nmi, r.interval_start, r.value, r.uom)
80
+ ```
81
+
82
+ Or as a flat CSV / DataFrame:
83
+
84
+ ```python
85
+ from aemo_mdff_reader import parse, write_csv, to_dataframe
86
+
87
+ write_csv(parse("metering.csv"), "out.csv") # no pandas
88
+ df = to_dataframe("metering.csv") # needs [pandas]
89
+ ```
90
+
91
+ From the command line:
92
+
93
+ ```bash
94
+ aemo-mdff-reader metering.csv -o out.csv
95
+ aemo-mdff-reader metering.csv --validate # spec check
96
+ aemo-mdff-reader metering.csv --nmi NMI1234567 --start 2024-01-01 --end 2024-01-31
97
+ aemo-mdff-reader manual.csv --records accumulations # NEM13
98
+ ```
99
+
100
+ ## Working with the data
101
+
102
+ Each parsed record is a slots-based class with named attributes plus a
103
+ `to_dict()` for JSON / dict pipelines:
104
+
105
+ ```python
106
+ for r in parse("metering.csv"):
107
+ payload = r.to_dict() # {"nmi": "...", "value": 0.12, ...}
108
+ print(r.quality_flag, r.method_flag) # split of the QMM field, e.g. "S", "52"
109
+ ```
110
+
111
+ For aggregation, `aemo_mdff_reader.aggregate` provides streaming helpers:
112
+
113
+ ```python
114
+ from aemo_mdff_reader import parse
115
+ from aemo_mdff_reader.aggregate import group_by_nmi, daily_totals
116
+
117
+ for key, group in group_by_nmi(parse("metering.csv")):
118
+ # key = ChannelKey(nmi, register_id, nmi_suffix)
119
+ intervals = list(group)
120
+
121
+ for day in daily_totals(parse("metering.csv")):
122
+ # day.total, day.interval_count, day.unique_quality_flags
123
+ print(day.nmi, day.interval_date.date(), day.total, day.uom)
124
+ ```
125
+
126
+ End-to-end recipes — load + inspect, daily roll-up, filter to pandas,
127
+ spec validation — live in [`examples/`](examples/).
128
+
129
+ ## API at a glance
130
+
131
+ | You want | Call |
132
+ | ------------------------------------- | ----------------------------- |
133
+ | 300 interval readings (NEM12) | `parse(src)` |
134
+ | 250 accumulations (NEM13) | `parse_accumulations(src)` |
135
+ | Both, in file order | `parse_all(src)` |
136
+ | 400 quality / event flags | `parse_events(src)` |
137
+ | 500 / 550 B2B transactions | `parse_b2b(src)` |
138
+ | Just the 100 header | `parse_header(src)` |
139
+ | Build a pandas DataFrame | `to_dataframe(src)` |
140
+ | Write a flat CSV (no pandas) | `write_csv(rows, out)` |
141
+ | Validate against AEMO MDFF v2.6 | `validate_file(src)` |
142
+ | Compute / verify an NMI checksum | `nmi_checksum`, `validate_nmi`|
143
+ | Group readings by NMI / channel | `aggregate.group_by_nmi(rows)`|
144
+ | Roll up to daily totals | `aggregate.daily_totals(rows)`|
145
+ | Convert any record to a plain dict | `r.to_dict()` |
146
+
147
+ `src` can be a path, a file-like object, an iterable of CSV lines, or
148
+ an iterable of pre-split rows. The v1 `NEMReader` facade
149
+ (`read_from_file`, `to_dataframe`, `to_csv`) still works.
150
+
151
+ Each `parse(...)` yields an `IntervalReading` with `nmi`,
152
+ `meter_serial_number`, `register_id`, `nmi_suffix`, `uom`,
153
+ `interval_length`, `interval_date`, `interval_start`, `interval_end`,
154
+ `interval_index`, `value`, `quality_method`, `reason_code`,
155
+ `reason_description`, `update_datetime`, `msats_load_datetime`. See
156
+ the type stubs (`from aemo_mdff_reader import IntervalReading`) for the
157
+ exact signatures.
158
+
159
+ ## Notes
160
+
161
+ - **Spec**: AEMO Meter Data File Format Specification NEM12 & NEM13,
162
+ **v2.6** (effective 29 September 2024). Records `100`, `200`, `250`,
163
+ `300`, `400`, `500`, `550`, `900` are all surfaced; unknown indicators
164
+ are ignored. Allowed values for quality flags, transaction codes,
165
+ reason codes, units of measure, and direction indicators are exposed
166
+ as constants in `aemo_mdff_reader.spec` for callers that want stricter
167
+ validation than the parser performs.
168
+ - **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
171
+ forms (`YYYYMMDD`, `YYYYMMDDhhmmss`) and a few common non-spec
172
+ variants (`YYYY-MM-DD`, ISO `YYYY-MM-DDTHH:MM:SS`, with or without a
173
+ `Z` / `±HH:MM` / `±HHMM` timezone suffix — the suffix is stripped
174
+ and parsed datetimes are returned naive). `direction_indicator` on
175
+ 250 records passes through whatever the file emits; the spec set is
176
+ `spec.DIRECTION_INDICATORS = {"I", "E"}` but `B` and `N` appear in
177
+ the wild. The parser also accepts non-spec IntervalLengths (1, 60,
178
+ etc.); strict callers should compare against
179
+ `spec.ALLOWED_INTERVAL_LENGTHS` (= `{5, 15, 30}`).
180
+ - **Migration from v1**: `NEMReader` still works. The internal
181
+ `aemo_mdff_reader.nemstructure` package is gone — see the API table
182
+ above. `pandas` is now opt-in. See `CHANGELOG.md` for details.
183
+
184
+ ## Performance
185
+
186
+ 420,480 readings (4 NMIs × 365 days × 5-min, 2.8 MiB CSV), Python 3.11:
187
+
188
+ | Operation | Time |
189
+ | ---------------------------------- | -----: |
190
+ | `for r in parse(path): ...` | 0.45 s |
191
+ | `parse_to_columns(path)` | 0.21 s |
192
+ | `to_dataframe(path)` (pandas) | 0.76 s |
193
+
194
+ ~2.7× faster than v1 end-to-end; reproduce with
195
+ `python benchmarks/bench_parser.py`.
196
+
197
+ ## Large files
198
+
199
+ The parser is built to scale to gigabyte-class NEM12 files without
200
+ loading them into RAM. Measured peak memory delta on a synthetic
201
+ **10.5 M-reading file** (100 NMIs × 365 days × 5-min, 71 MiB CSV),
202
+ Python 3.12:
203
+
204
+ | API | Memory profile | Peak Δ |
205
+ | ---------------------------------------- | -------------- | -----: |
206
+ | `for r in parse(path): ...` | streaming | **1.3 MiB** |
207
+ | `daily_totals(parse(path))` | streaming | **0 MiB** |
208
+ | `write_csv(parse(path), out)` | streaming | **0 MiB** |
209
+ | `iter_dataframes(path, chunk_size=N)` | bounded O(N) | **~30 MiB / 100k** |
210
+ | `iter_columns_chunks(path, chunk_size=N)`| bounded O(N) | **~10 MiB / 100k** |
211
+ | `parse_to_columns(path)` | full materialise | ~600 MiB |
212
+ | `list(parse(path))` / `to_dataframe(path)` / `NEMReader.read_from_file()` | full materialise | ~2.5 GiB |
213
+
214
+ Rule of thumb: stay on the streaming or chunked APIs for any file
215
+ larger than a few hundred MiB. The chunked variants make pandas-based
216
+ workflows safe on arbitrarily large inputs:
217
+
218
+ ```python
219
+ from aemo_mdff_reader import iter_dataframes
220
+
221
+ # Process a multi-GiB file 50,000 readings at a time.
222
+ for df in iter_dataframes("huge.csv", chunk_size=50_000):
223
+ daily = df.groupby(["NMI", "IntervalDate"])["Value"].sum()
224
+ daily.to_csv("out.csv", mode="a", header=False)
225
+ ```
226
+
227
+ The `NEMReader` facade and `to_dataframe(path)` materialise their
228
+ inputs by design (so `len(reader)` and `df.iloc[...]` work). Avoid
229
+ them for files that won't fit in RAM.
230
+
231
+ ## Development
232
+
233
+ ```bash
234
+ git clone https://github.com/Utilified/aemo-mdff-reader.git
235
+ cd aemo-mdff-reader
236
+ pip install -e .[dev]
237
+ pytest
238
+ ```
239
+
240
+ CI runs ruff, mypy --strict, the test matrix on Python 3.9 → 3.12 /
241
+ Linux / macOS / Windows, `pip-audit`, `bandit`, and a wheel-install
242
+ smoke test. Releases are signed with sigstore and published to PyPI
243
+ via Trusted Publishing on tag.
244
+
245
+ ## License
246
+
247
+ MIT — see [LICENSE](LICENSE).