aemo-mdff-reader 2.0.2__tar.gz → 2.1.0__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 (43) hide show
  1. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/CHANGELOG.md +61 -0
  2. aemo_mdff_reader-2.1.0/CONTRIBUTING.md +222 -0
  3. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/PKG-INFO +15 -8
  4. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/README.md +13 -5
  5. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/aemo_mdff_reader/__init__.py +1 -1
  6. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/aemo_mdff_reader/cli.py +1 -0
  7. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/aemo_mdff_reader/sql/__init__.py +6 -0
  8. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/aemo_mdff_reader/sql/store.py +11 -3
  9. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/aemo_mdff_reader.egg-info/PKG-INFO +15 -8
  10. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/pyproject.toml +6 -10
  11. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/tests/test_parser.py +6 -3
  12. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/tests/test_spec.py +7 -2
  13. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/tests/test_sql.py +1 -1
  14. aemo_mdff_reader-2.0.2/CONTRIBUTING.md +0 -118
  15. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/LICENSE +0 -0
  16. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/MANIFEST.in +0 -0
  17. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/aemo_mdff_reader/aggregate.py +0 -0
  18. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/aemo_mdff_reader/parser.py +0 -0
  19. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/aemo_mdff_reader/py.typed +0 -0
  20. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/aemo_mdff_reader/reader.py +0 -0
  21. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/aemo_mdff_reader/spec.py +0 -0
  22. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/aemo_mdff_reader/sql/query.py +0 -0
  23. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/aemo_mdff_reader/types.py +0 -0
  24. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/aemo_mdff_reader.egg-info/SOURCES.txt +0 -0
  25. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/aemo_mdff_reader.egg-info/dependency_links.txt +0 -0
  26. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/aemo_mdff_reader.egg-info/entry_points.txt +0 -0
  27. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/aemo_mdff_reader.egg-info/requires.txt +0 -0
  28. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/aemo_mdff_reader.egg-info/top_level.txt +0 -0
  29. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/benchmarks/bench_parser.py +0 -0
  30. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/setup.cfg +0 -0
  31. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/setup.py +0 -0
  32. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/tests/__init__.py +0 -0
  33. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/tests/fixtures/generate.py +0 -0
  34. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/tests/fixtures/sample_nem12.csv +0 -0
  35. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/tests/fixtures/sample_nem13.csv +0 -0
  36. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/tests/test_cli.py +0 -0
  37. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/tests/test_datetime_formats.py +0 -0
  38. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/tests/test_edge_cases.py +0 -0
  39. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/tests/test_ergonomics.py +0 -0
  40. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/tests/test_events_b2b.py +0 -0
  41. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/tests/test_large_files.py +0 -0
  42. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/tests/test_nem13.py +0 -0
  43. {aemo_mdff_reader-2.0.2 → aemo_mdff_reader-2.1.0}/tests/test_validation.py +0 -0
@@ -4,6 +4,67 @@ 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
+ ## [2.1.0](https://github.com/Utilified/aemo-mdff-reader/compare/v2.0.4...v2.1.0) (2026-05-10)
8
+
9
+
10
+ ### Features
11
+
12
+ * drop Python 3.9, hash-pin CI installs, scope release-please permissions ([447ec98](https://github.com/Utilified/aemo-mdff-reader/commit/447ec98408edabfab45d28841a2e714d54f8dd11))
13
+
14
+
15
+ ### Documentation
16
+
17
+ * remove broken OpenSSF Best Practices badge ([#21](https://github.com/Utilified/aemo-mdff-reader/issues/21)) ([91a68d5](https://github.com/Utilified/aemo-mdff-reader/commit/91a68d5a4b4fd4c027488b4fe770c4cd142a7454))
18
+
19
+ ## [2.0.4](https://github.com/Utilified/aemo-mdff-reader/compare/v2.0.3...v2.0.4) (2026-05-10)
20
+
21
+ First release published to PyPI after the supply-chain hardening landed
22
+ in v2.0.3 — which itself failed to publish due to two release-pipeline
23
+ bugs (see notice on the v2.0.3 GitHub Release). PyPI users upgrading
24
+ from 2.0.2 receive **both** the v2.0.3 hardening (SLSA build provenance,
25
+ CycloneDX SBOM, release-please automation, dependabot grouping) and the
26
+ fixes below in one upgrade. No source or behavioural changes versus 2.0.2.
27
+
28
+ ### Bug Fixes
29
+
30
+ * release-please dispatch + SBOM directory creation ([#17](https://github.com/Utilified/aemo-mdff-reader/issues/17)) ([8e3cd13](https://github.com/Utilified/aemo-mdff-reader/commit/8e3cd13ca11bad3399efd2e5fd2fe5ae6bcb2a90))
31
+
32
+ ## [2.0.3](https://github.com/Utilified/aemo-mdff-reader/compare/v2.0.2...v2.0.3) (2026-05-10)
33
+
34
+ Supply-chain hardening and release-process automation. No source or
35
+ behavioural changes versus 2.0.2 — same wheel/sdist content, plus
36
+ provenance, SBOM, and an automated release pipeline.
37
+
38
+ ### Added
39
+
40
+ - SLSA build provenance attestations on the wheel and sdist (via
41
+ `actions/attest-build-provenance`). Verifiable with
42
+ `gh attestation verify`.
43
+ - CycloneDX SBOM generated by `anchore/sbom-action` and attested
44
+ against both artifacts (via `actions/attest-sbom`). SBOM is also
45
+ attached to each GitHub Release alongside the sigstore signatures.
46
+ - `release-please` automation: releases are now proposed and cut
47
+ from Conventional Commits. Merging the auto-generated release PR
48
+ creates the tag, which dispatches the existing `release.yml`
49
+ pipeline. See [CONTRIBUTING.md](CONTRIBUTING.md) for the flow.
50
+ - Dependabot grouping: minor/patch GitHub Actions bumps land as a
51
+ single weekly PR; minor/patch runtime-extras bumps (pandas /
52
+ pyarrow / PyMySQL) land as a separate weekly PR. Majors stay
53
+ individual.
54
+ - README badges for CodeQL, OpenSSF Scorecard, and OpenSSF Best
55
+ Practices.
56
+
57
+ ### Changed
58
+
59
+ - Branch protection on `main` now requires PR conversation resolution
60
+ before merge.
61
+ - GitHub Discussions enabled on the repository as a Q&A surface
62
+ separate from the issue tracker.
63
+
64
+ ### Bug Fixes
65
+
66
+ * rename release-please config to default path ([#15](https://github.com/Utilified/aemo-mdff-reader/issues/15)) ([843d25f](https://github.com/Utilified/aemo-mdff-reader/commit/843d25ff8c35944e08eb70ebfdb076acdf1c9855))
67
+
7
68
  ## [Unreleased]
8
69
 
9
70
  ## [2.0.2] — 2026-05-10
@@ -0,0 +1,222 @@
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
+ ## CI requirements files
28
+
29
+ `requirements/{dev,lint,build,audit}.txt` are hash-pinned lock files
30
+ consumed by CI (`pip install --require-hashes`). They satisfy
31
+ Scorecard's `PinnedDependenciesID` and protect CI against a compromised
32
+ PyPI mirror. Dependabot refreshes them weekly.
33
+
34
+ Source `.in` files are committed alongside the locks in `requirements/`.
35
+ To regenerate manually after editing them or `pyproject.toml`:
36
+
37
+ ```bash
38
+ pip install uv
39
+ uv pip compile --generate-hashes --strip-extras --python-version 3.10 --output-file requirements/dev.txt requirements/dev.in
40
+ uv pip compile --generate-hashes --strip-extras --python-version 3.10 --output-file requirements/lint.txt requirements/lint.in
41
+ uv pip compile --generate-hashes --strip-extras --python-version 3.10 --output-file requirements/build.txt requirements/build.in
42
+ uv pip compile --generate-hashes --strip-extras --python-version 3.10 --output-file requirements/audit.txt requirements/audit.in
43
+ ```
44
+
45
+ `uv` is used over `pip-tools` because its resolver handles the
46
+ 3.10–3.12 matrix without back-tracking failures, and it emits hashes
47
+ for every wheel of each pinned version (so a single lock file works
48
+ across the whole CI matrix). The `--python-version 3.10` flag pins
49
+ resolution to the lowest supported interpreter so packages whose
50
+ latest releases dropped 3.10 are correctly stepped back to a
51
+ compatible version.
52
+
53
+ ## Running the test suite
54
+
55
+ ```bash
56
+ pytest -v
57
+ pytest --cov=aemo_mdff_reader --cov-report=term-missing
58
+ ```
59
+
60
+ A small, sanitized NEM12 sample lives at
61
+ `tests/fixtures/sample_nem12.csv`. To generate larger synthetic files
62
+ for ad-hoc testing or benchmarking:
63
+
64
+ ```bash
65
+ python tests/fixtures/generate.py /tmp/big.csv --nmis 4 --days 365 --interval-minutes 5
66
+ ```
67
+
68
+ ## Performance
69
+
70
+ `benchmarks/bench_parser.py` is the canonical performance test. Please
71
+ run it before and after changes that touch the hot path
72
+ (`parser.py::_emit_intervals`, `parse_to_columns`, `to_columns`):
73
+
74
+ ```bash
75
+ python benchmarks/bench_parser.py --nmis 4 --days 365 --interval-minutes 5
76
+ ```
77
+
78
+ The streaming and columnar fast paths must remain at or above the
79
+ current published throughput.
80
+
81
+ ## Code style
82
+
83
+ - Pure stdlib in the core. **No** new required runtime dependencies.
84
+ Optional features go behind `[project.optional-dependencies]`.
85
+ - Type hints on all public symbols.
86
+ - Keep `aemo_mdff_reader.types` slots-based — per-row allocation is on
87
+ the hot path.
88
+
89
+ ## Commit messages
90
+
91
+ Releases are generated automatically from commits using
92
+ [release-please](https://github.com/googleapis/release-please), so the
93
+ commit subject prefix is load-bearing — it determines whether a release
94
+ is cut and what kind of version bump it gets.
95
+
96
+ Use [Conventional Commits](https://www.conventionalcommits.org/):
97
+
98
+ | Prefix | Effect on next release |
99
+ | ---------------------------------- | -------------------------------------- |
100
+ | `feat: …` | minor bump (e.g. 2.0.x → 2.1.0) |
101
+ | `fix: …` | patch bump (e.g. 2.0.0 → 2.0.1) |
102
+ | `feat!: …` or `BREAKING CHANGE:` footer | major bump (e.g. 2.x → 3.0.0) |
103
+ | `chore: …` / `docs: …` / `refactor: …` / `style: …` / `test: …` / `ci: …` / `build: …` | no release on its own |
104
+ | any commit with `Release-As: x.y.z` footer | forces that exact version |
105
+
106
+ Subject line: imperative mood, under 72 chars, lowercase after the
107
+ prefix. Examples that work well:
108
+
109
+ ```
110
+ feat: stream NEM13 250 records via parse_accumulations
111
+ fix: handle CRLF line endings in 300-record payloads
112
+ chore: bump CI matrix to include Python 3.13
113
+ ```
114
+
115
+ Squash-merge is preferred — the squash commit message becomes the
116
+ release-please input, so make it count.
117
+
118
+ ## Reporting issues
119
+
120
+ Please open an issue with:
121
+
122
+ 1. The Python version and OS.
123
+ 2. A minimal reproducer (a few NEM12 rows is usually enough).
124
+ 3. The expected vs actual behaviour.
125
+
126
+ If you can share a sanitized sample file (no real NMIs / meter
127
+ serials), please attach it to the issue.
128
+
129
+ ## Releasing
130
+
131
+ Releases are automated via [release-please](https://github.com/googleapis/release-please).
132
+ Maintainers do not run `git tag` by hand for normal releases.
133
+
134
+ ### Normal flow (Conventional Commits drive it)
135
+
136
+ 1. Land a `feat:` or `fix:` commit on `main` (via squash-merged PR).
137
+ 2. The `release-please` workflow runs and either opens a new release PR
138
+ titled `chore(main): release X.Y.Z` or updates the existing one.
139
+ The PR auto-bumps `pyproject.toml` and `aemo_mdff_reader/__init__.py`
140
+ (the `# x-release-please-version` annotation in `__init__.py` marks
141
+ the line release-please rewrites) and drafts the `CHANGELOG.md`
142
+ entry from the commit log.
143
+ 3. Review the release PR. Edit the changelog entry directly on the PR
144
+ if you want to massage wording — release-please will keep your
145
+ edits across reruns. When happy, **squash-merge** it.
146
+ 4. release-please creates the `vX.Y.Z` git tag on merge.
147
+ 5. Because tags created by `GITHUB_TOKEN` do not trigger downstream
148
+ tag-push workflows (GitHub anti-recursion guard), `release-please.yml`
149
+ explicitly dispatches `release.yml` for the new tag.
150
+ 6. `release.yml` runs the publish pipeline:
151
+ build → SLSA build provenance attestation → CycloneDX SBOM +
152
+ attestation → upload artefacts → wait on the `pypi` Environment
153
+ reviewer → sigstore sign → PyPI Trusted Publishing → GitHub Release
154
+ with wheel + sdist + signatures + SBOM attached.
155
+ 7. Approve the deployment in the `pypi` Environment when GitHub
156
+ notifies you. The publish completes; verify at
157
+ https://pypi.org/project/aemo-mdff-reader/ and
158
+ https://github.com/Utilified/aemo-mdff-reader/releases.
159
+
160
+ ### Forcing a release without a feat/fix commit
161
+
162
+ Two options:
163
+
164
+ - Add `Release-As: x.y.z` as a commit-message footer on a commit
165
+ landing on `main` (works for `chore:` / `docs:` etc. that wouldn't
166
+ trigger a release on their own).
167
+ - Manually trigger the `release-please` workflow via GitHub Actions →
168
+ *release-please* → "Run workflow" (workflow_dispatch).
169
+
170
+ ### Emergency manual release (rare)
171
+
172
+ `release.yml` still listens on tag-push, so a maintainer can bypass
173
+ release-please entirely:
174
+
175
+ ```bash
176
+ # Bump version in pyproject.toml AND aemo_mdff_reader/__init__.py
177
+ # (preserve the `# x-release-please-version` comment on __init__.py),
178
+ # update CHANGELOG.md, then:
179
+ git commit -am "release: vX.Y.Z"
180
+ git tag vX.Y.Z
181
+ git push origin main vX.Y.Z
182
+ ```
183
+
184
+ Use only if release-please is broken — otherwise you'll fight it on
185
+ the next push.
186
+
187
+ ### One-time infrastructure setup (per project)
188
+
189
+ Already done for `aemo-mdff-reader`; documented here for posterity.
190
+
191
+ 1. **PyPI Trusted Publishing**: register the project at
192
+ https://pypi.org/manage/account/publishing/ with:
193
+ - PyPI project name: `aemo-mdff-reader`
194
+ - Owner: `Utilified`
195
+ - Repository: `aemo-mdff-reader`
196
+ - Workflow filename: `release.yml`
197
+ - Environment name: `pypi`
198
+ 2. **GitHub Environment**: in *Settings → Environments*, create an
199
+ environment named `pypi`. Add required reviewers (recommended) and
200
+ restrict the deployment branch policy to `v*` tags.
201
+ 3. **Repository variable**: set `PYPI_PUBLISH_ENABLED=true` under
202
+ *Settings → Secrets and variables → Actions → Variables*. The
203
+ publish job is gated on this so the workflow runs cleanly while
204
+ Trusted Publishing is being configured (build + sign succeed;
205
+ publish skips).
206
+ 4. **Workflow permissions**: under *Settings → Actions → General →
207
+ Workflow permissions*, set "Read and write permissions" and tick
208
+ "Allow GitHub Actions to create and approve pull requests" — the
209
+ `release-please` action needs both to open release PRs.
210
+
211
+ ### Local dry run of the build
212
+
213
+ ```bash
214
+ python -m build
215
+ twine check --strict dist/*
216
+ python -m venv /tmp/smoke && /tmp/smoke/bin/pip install dist/*.whl
217
+ /tmp/smoke/bin/aemo-mdff-reader --version
218
+ ```
219
+
220
+ There is no way to dry-run the publish step — Trusted Publishing only
221
+ authenticates a real `release.yml` run on a real tag. The first
222
+ release after touching `release.yml` is the de-facto integration test.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aemo-mdff-reader
3
- Version: 2.0.2
3
+ Version: 2.1.0
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,7 +15,6 @@ 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.9
19
18
  Classifier: Programming Language :: Python :: 3.10
20
19
  Classifier: Programming Language :: Python :: 3.11
21
20
  Classifier: Programming Language :: Python :: 3.12
@@ -23,7 +22,7 @@ Classifier: Programming Language :: Python :: 3.13
23
22
  Classifier: Topic :: Office/Business :: Financial
24
23
  Classifier: Topic :: Scientific/Engineering
25
24
  Classifier: Topic :: Utilities
26
- Requires-Python: >=3.9
25
+ Requires-Python: >=3.10
27
26
  Description-Content-Type: text/markdown
28
27
  License-File: LICENSE
29
28
  Provides-Extra: pandas
@@ -49,8 +48,10 @@ Dynamic: license-file
49
48
 
50
49
  [![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
50
  [![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/)
51
+ [![Python versions](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue)](https://pypi.org/project/aemo-mdff-reader/)
53
52
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
53
+ [![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)
54
+ [![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)
54
55
 
55
56
  Fast, zero-dependency streaming reader for AEMO **NEM12** and **NEM13**
56
57
  metering files. Implements AEMO MDFF (Meter Data File Format) v2.6.
@@ -237,10 +238,16 @@ pip install -e .[dev]
237
238
  pytest
238
239
  ```
239
240
 
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.
241
+ CI runs ruff, mypy --strict, the test matrix on Python 3.10 → 3.12 /
242
+ Linux / macOS / Windows, `pip-audit`, `bandit`, CodeQL, OpenSSF
243
+ Scorecard, and a wheel-install smoke test.
244
+
245
+ Releases are automated by [release-please](https://github.com/googleapis/release-please)
246
+ from [Conventional Commits](https://www.conventionalcommits.org/) on
247
+ `main`, then signed with sigstore, attested with SLSA build provenance
248
+ and a CycloneDX SBOM, and published to PyPI via Trusted Publishing.
249
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the contributor commit
250
+ conventions and the full release flow.
244
251
 
245
252
  ## License
246
253
 
@@ -2,8 +2,10 @@
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/pypi/pyversions/aemo-mdff-reader.svg)](https://pypi.org/project/aemo-mdff-reader/)
5
+ [![Python versions](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.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
+ [![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
+ [![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)
7
9
 
8
10
  Fast, zero-dependency streaming reader for AEMO **NEM12** and **NEM13**
9
11
  metering files. Implements AEMO MDFF (Meter Data File Format) v2.6.
@@ -190,10 +192,16 @@ pip install -e .[dev]
190
192
  pytest
191
193
  ```
192
194
 
193
- CI runs ruff, mypy --strict, the test matrix on Python 3.9 → 3.12 /
194
- Linux / macOS / Windows, `pip-audit`, `bandit`, and a wheel-install
195
- smoke test. Releases are signed with sigstore and published to PyPI
196
- via Trusted Publishing on tag.
195
+ CI runs ruff, mypy --strict, the test matrix on Python 3.10 → 3.12 /
196
+ Linux / macOS / Windows, `pip-audit`, `bandit`, CodeQL, OpenSSF
197
+ Scorecard, and a wheel-install smoke test.
198
+
199
+ Releases are automated by [release-please](https://github.com/googleapis/release-please)
200
+ from [Conventional Commits](https://www.conventionalcommits.org/) on
201
+ `main`, then signed with sigstore, attested with SLSA build provenance
202
+ and a CycloneDX SBOM, and published to PyPI via Trusted Publishing.
203
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the contributor commit
204
+ conventions and the full release flow.
197
205
 
198
206
  ## License
199
207
 
@@ -48,7 +48,7 @@ from .types import (
48
48
  NMIDetails,
49
49
  )
50
50
 
51
- __version__ = "2.0.2"
51
+ __version__ = "2.1.0" # x-release-please-version
52
52
 
53
53
  __all__ = [
54
54
  "INTERVAL_DATA_OUTPUT_HEADERS",
@@ -223,6 +223,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
223
223
  try:
224
224
  sys.stdout.flush()
225
225
  except BrokenPipeError:
226
+ # Pipe is already gone; nothing to flush.
226
227
  pass
227
228
  return 0
228
229
 
@@ -7,8 +7,14 @@ The dependency is only required when you actually instantiate
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ from typing import TYPE_CHECKING
11
+
10
12
  from .query import COLUMNS, QueryBuilder
11
13
 
14
+ if TYPE_CHECKING:
15
+ # Visible to static analysers / IDEs without forcing pymysql to load.
16
+ from .store import DBCredentials, Storer
17
+
12
18
  __all__ = ["COLUMNS", "DBCredentials", "QueryBuilder", "Storer"]
13
19
 
14
20
 
@@ -4,7 +4,15 @@
4
4
 
5
5
  import pymysql.cursors
6
6
 
7
- from .query import *
7
+ from .query import (
8
+ CHANNEL_TABLE,
9
+ IMPORT_TABLE,
10
+ INTERVAL_DATA_TABLE,
11
+ INTERVAL_TABLE,
12
+ INTERVAL_TYPE_TABLE,
13
+ NMI_TABLE,
14
+ QueryBuilder,
15
+ )
8
16
 
9
17
  # Load queries once session is imported
10
18
  QUERY_FOLDER = "queries"
@@ -128,11 +136,11 @@ class Storer:
128
136
 
129
137
  data_intervals.append(data_interval)
130
138
  query = QueryBuilder.insert_query(NMI_TABLE, data_nmi)
131
- resultset = cursor.execute(query)
139
+ cursor.execute(query)
132
140
 
133
141
  for row in range(len(data_channel)):
134
142
  query = QueryBuilder.insert_query(CHANNEL_TABLE, [data_channel[row]])
135
- resultset = cursor.execute(query)
143
+ cursor.execute(query)
136
144
  query = "SELECT channelID FROM `nem`.`channel` WHERE `NMI` = %s AND `NMISuffix` = %s AND `MeterSerialNumber` = %s"
137
145
  cursor.execute(
138
146
  query, (data_channel[row][0], data_channel[row][2], data_channel[row][4])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aemo-mdff-reader
3
- Version: 2.0.2
3
+ Version: 2.1.0
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,7 +15,6 @@ 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.9
19
18
  Classifier: Programming Language :: Python :: 3.10
20
19
  Classifier: Programming Language :: Python :: 3.11
21
20
  Classifier: Programming Language :: Python :: 3.12
@@ -23,7 +22,7 @@ Classifier: Programming Language :: Python :: 3.13
23
22
  Classifier: Topic :: Office/Business :: Financial
24
23
  Classifier: Topic :: Scientific/Engineering
25
24
  Classifier: Topic :: Utilities
26
- Requires-Python: >=3.9
25
+ Requires-Python: >=3.10
27
26
  Description-Content-Type: text/markdown
28
27
  License-File: LICENSE
29
28
  Provides-Extra: pandas
@@ -49,8 +48,10 @@ Dynamic: license-file
49
48
 
50
49
  [![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
50
  [![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/)
51
+ [![Python versions](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue)](https://pypi.org/project/aemo-mdff-reader/)
53
52
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
53
+ [![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)
54
+ [![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)
54
55
 
55
56
  Fast, zero-dependency streaming reader for AEMO **NEM12** and **NEM13**
56
57
  metering files. Implements AEMO MDFF (Meter Data File Format) v2.6.
@@ -237,10 +238,16 @@ pip install -e .[dev]
237
238
  pytest
238
239
  ```
239
240
 
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.
241
+ CI runs ruff, mypy --strict, the test matrix on Python 3.10 → 3.12 /
242
+ Linux / macOS / Windows, `pip-audit`, `bandit`, CodeQL, OpenSSF
243
+ Scorecard, and a wheel-install smoke test.
244
+
245
+ Releases are automated by [release-please](https://github.com/googleapis/release-please)
246
+ from [Conventional Commits](https://www.conventionalcommits.org/) on
247
+ `main`, then signed with sigstore, attested with SLSA build provenance
248
+ and a CycloneDX SBOM, and published to PyPI via Trusted Publishing.
249
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the contributor commit
250
+ conventions and the full release flow.
244
251
 
245
252
  ## License
246
253
 
@@ -11,14 +11,15 @@ build-backend = "setuptools.build_meta"
11
11
  # Pillow / PIL precedent (``pip install Pillow`` → ``import PIL``)
12
12
  # applies here.
13
13
  name = "aemo-mdff-reader"
14
- version = "2.0.2"
14
+ version = "2.1.0"
15
15
  description = "Fast streaming reader for AEMO MDFF NEM12 / NEM13 metering files. Zero required dependencies."
16
16
  readme = "README.md"
17
17
  license = "MIT"
18
18
  license-files = ["LICENSE"]
19
- # Python 3.8 is EOL (Oct 2024) and setuptools 77+ — required for the
20
- # PEP 639 license metadata above — does not publish a 3.8 wheel.
21
- requires-python = ">=3.9"
19
+ # Python 3.9 reached end-of-life in October 2025; CI hash-pinning of
20
+ # pandas/pyarrow/numpy no longer resolves cleanly against 3.9 wheels.
21
+ # 3.10 is the lowest supported runtime as of v3.0.
22
+ requires-python = ">=3.10"
22
23
  authors = [
23
24
  { name = "Utilified Holdings Pty Ltd" },
24
25
  ]
@@ -30,7 +31,6 @@ classifiers = [
30
31
  "Operating System :: OS Independent",
31
32
  "Programming Language :: Python :: 3",
32
33
  "Programming Language :: Python :: 3 :: Only",
33
- "Programming Language :: Python :: 3.9",
34
34
  "Programming Language :: Python :: 3.10",
35
35
  "Programming Language :: Python :: 3.11",
36
36
  "Programming Language :: Python :: 3.12",
@@ -95,7 +95,7 @@ exclude_lines = [
95
95
 
96
96
  [tool.ruff]
97
97
  line-length = 100
98
- target-version = "py39"
98
+ target-version = "py310"
99
99
 
100
100
  [tool.ruff.lint]
101
101
  select = [
@@ -128,10 +128,6 @@ ignore = [
128
128
  "aemo_mdff_reader/sql/*" = ["F401", "F403", "F405", "UP031"]
129
129
 
130
130
  [tool.mypy]
131
- # Lowest supported runtime is 3.9 (see ``requires-python``), but mypy
132
- # 1.16+ no longer accepts ``<3.10`` here. Type-checking against 3.10
133
- # still produces useful results — we don't use any 3.10-only typing
134
- # features (no ``match`` statements, no PEP 604 ``X | Y`` at runtime).
135
131
  python_version = "3.10"
136
132
  strict = true
137
133
  files = ["aemo_mdff_reader"]
@@ -127,7 +127,8 @@ def test_parse_to_columns_matches_to_columns():
127
127
 
128
128
 
129
129
  def test_parse_to_columns_from_iterable():
130
- rows = list(csv.reader(open(FIXTURE)))
130
+ with open(FIXTURE) as f:
131
+ rows = list(csv.reader(f))
131
132
  a = parse_to_columns(FIXTURE)
132
133
  b = parse_to_columns(rows)
133
134
  assert a == b
@@ -178,7 +179,8 @@ def test_header_repr():
178
179
 
179
180
 
180
181
  def test_nem_reader_read_from_array():
181
- rows = list(csv.reader(open(FIXTURE)))
182
+ with open(FIXTURE) as f:
183
+ rows = list(csv.reader(f))
182
184
  rdr = NEMReader()
183
185
  rdr.read_from_array(rows)
184
186
  assert len(rdr.readings) == 3 * 48
@@ -188,7 +190,8 @@ def test_nem_reader_read_from_array():
188
190
  def test_nem_reader_to_columns_without_filename():
189
191
  # NEMReader.to_columns falls through the Reading-iterable branch
190
192
  # when no source filename is buffered.
191
- rows = list(csv.reader(open(FIXTURE)))
193
+ with open(FIXTURE) as f:
194
+ rows = list(csv.reader(f))
192
195
  rdr = NEMReader()
193
196
  rdr.read_from_array(rows)
194
197
  cols = rdr.to_columns()
@@ -2,8 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from urllib.parse import urlparse
6
+
5
7
  import aemo_mdff_reader
6
- from aemo_mdff_reader import spec
8
+
9
+ spec = aemo_mdff_reader.spec
7
10
 
8
11
 
9
12
  def test_spec_module_is_re_exported():
@@ -16,7 +19,9 @@ def test_spec_version_metadata():
16
19
  # ISO-8601 effective date keeps callers parseable.
17
20
  assert spec.SPEC_EFFECTIVE_DATE == "2024-09-29"
18
21
  assert "Meter Data File Format" in spec.SPEC_DOCUMENT_TITLE
19
- assert spec.SPEC_URL.startswith("https://www.aemo.com.au")
22
+ parsed = urlparse(spec.SPEC_URL)
23
+ assert parsed.scheme == "https"
24
+ assert parsed.hostname == "www.aemo.com.au"
20
25
 
21
26
 
22
27
  def test_allowed_interval_lengths_match_spec():
@@ -43,7 +43,7 @@ def test_storer_attribute_lazy_loads():
43
43
  # If pymysql is not installed, accessing Storer should raise ImportError
44
44
  # only at access time, not at module import time.
45
45
  pytest.importorskip("aemo_mdff_reader.sql")
46
- import aemo_mdff_reader.sql as sql_mod
46
+ from aemo_mdff_reader import sql as sql_mod
47
47
 
48
48
  if "pymysql" in sys.modules:
49
49
  # If pymysql is available in the test env, we can at least confirm
@@ -1,118 +0,0 @@
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
- ```