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.
- aemo_mdff_reader-2.0.2/CHANGELOG.md +182 -0
- aemo_mdff_reader-2.0.2/CONTRIBUTING.md +118 -0
- aemo_mdff_reader-2.0.2/LICENSE +21 -0
- aemo_mdff_reader-2.0.2/MANIFEST.in +8 -0
- aemo_mdff_reader-2.0.2/PKG-INFO +247 -0
- aemo_mdff_reader-2.0.2/README.md +200 -0
- aemo_mdff_reader-2.0.2/aemo_mdff_reader/__init__.py +84 -0
- aemo_mdff_reader-2.0.2/aemo_mdff_reader/aggregate.py +168 -0
- aemo_mdff_reader-2.0.2/aemo_mdff_reader/cli.py +231 -0
- aemo_mdff_reader-2.0.2/aemo_mdff_reader/parser.py +1241 -0
- aemo_mdff_reader-2.0.2/aemo_mdff_reader/py.typed +0 -0
- aemo_mdff_reader-2.0.2/aemo_mdff_reader/reader.py +107 -0
- aemo_mdff_reader-2.0.2/aemo_mdff_reader/spec.py +242 -0
- aemo_mdff_reader-2.0.2/aemo_mdff_reader/sql/__init__.py +22 -0
- aemo_mdff_reader-2.0.2/aemo_mdff_reader/sql/query.py +102 -0
- aemo_mdff_reader-2.0.2/aemo_mdff_reader/sql/store.py +178 -0
- aemo_mdff_reader-2.0.2/aemo_mdff_reader/types.py +437 -0
- aemo_mdff_reader-2.0.2/aemo_mdff_reader.egg-info/PKG-INFO +247 -0
- aemo_mdff_reader-2.0.2/aemo_mdff_reader.egg-info/SOURCES.txt +40 -0
- aemo_mdff_reader-2.0.2/aemo_mdff_reader.egg-info/dependency_links.txt +1 -0
- aemo_mdff_reader-2.0.2/aemo_mdff_reader.egg-info/entry_points.txt +2 -0
- aemo_mdff_reader-2.0.2/aemo_mdff_reader.egg-info/requires.txt +21 -0
- aemo_mdff_reader-2.0.2/aemo_mdff_reader.egg-info/top_level.txt +1 -0
- aemo_mdff_reader-2.0.2/benchmarks/bench_parser.py +109 -0
- aemo_mdff_reader-2.0.2/pyproject.toml +147 -0
- aemo_mdff_reader-2.0.2/setup.cfg +4 -0
- aemo_mdff_reader-2.0.2/setup.py +5 -0
- aemo_mdff_reader-2.0.2/tests/__init__.py +0 -0
- aemo_mdff_reader-2.0.2/tests/fixtures/generate.py +104 -0
- aemo_mdff_reader-2.0.2/tests/fixtures/sample_nem12.csv +8 -0
- aemo_mdff_reader-2.0.2/tests/fixtures/sample_nem13.csv +5 -0
- aemo_mdff_reader-2.0.2/tests/test_cli.py +96 -0
- aemo_mdff_reader-2.0.2/tests/test_datetime_formats.py +68 -0
- aemo_mdff_reader-2.0.2/tests/test_edge_cases.py +167 -0
- aemo_mdff_reader-2.0.2/tests/test_ergonomics.py +313 -0
- aemo_mdff_reader-2.0.2/tests/test_events_b2b.py +107 -0
- aemo_mdff_reader-2.0.2/tests/test_large_files.py +136 -0
- aemo_mdff_reader-2.0.2/tests/test_nem13.py +155 -0
- aemo_mdff_reader-2.0.2/tests/test_parser.py +235 -0
- aemo_mdff_reader-2.0.2/tests/test_spec.py +70 -0
- aemo_mdff_reader-2.0.2/tests/test_sql.py +55 -0
- 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,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
|
+
[](https://github.com/Utilified/aemo-mdff-reader/actions/workflows/ci.yml)
|
|
51
|
+
[](https://pypi.org/project/aemo-mdff-reader/)
|
|
52
|
+
[](https://pypi.org/project/aemo-mdff-reader/)
|
|
53
|
+
[](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).
|