aemo-mcp 0.1.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_mcp-0.1.2/.github/dependabot.yml +39 -0
- aemo_mcp-0.1.2/.github/workflows/codeql.yml +32 -0
- aemo_mcp-0.1.2/.github/workflows/release.yml +54 -0
- aemo_mcp-0.1.2/.github/workflows/test.yml +47 -0
- aemo_mcp-0.1.2/.gitignore +30 -0
- aemo_mcp-0.1.2/CHANGELOG.md +133 -0
- aemo_mcp-0.1.2/CLAUDE.md +182 -0
- aemo_mcp-0.1.2/CODE_OF_CONDUCT.md +39 -0
- aemo_mcp-0.1.2/CONTRIBUTING.md +68 -0
- aemo_mcp-0.1.2/LICENSE +33 -0
- aemo_mcp-0.1.2/PKG-INFO +283 -0
- aemo_mcp-0.1.2/README.md +253 -0
- aemo_mcp-0.1.2/RESEARCH.md +238 -0
- aemo_mcp-0.1.2/SECURITY.md +49 -0
- aemo_mcp-0.1.2/examples/claude_desktop_config.json +8 -0
- aemo_mcp-0.1.2/examples/demo_prompts.md +54 -0
- aemo_mcp-0.1.2/glama.json +4 -0
- aemo_mcp-0.1.2/llms.txt +54 -0
- aemo_mcp-0.1.2/pyproject.toml +66 -0
- aemo_mcp-0.1.2/src/aemo_mcp/__init__.py +6 -0
- aemo_mcp-0.1.2/src/aemo_mcp/cache.py +139 -0
- aemo_mcp-0.1.2/src/aemo_mcp/client.py +237 -0
- aemo_mcp-0.1.2/src/aemo_mcp/curated.py +226 -0
- aemo_mcp-0.1.2/src/aemo_mcp/data/curated/daily_summary.yaml +68 -0
- aemo_mcp-0.1.2/src/aemo_mcp/data/curated/dispatch_price.yaml +54 -0
- aemo_mcp-0.1.2/src/aemo_mcp/data/curated/dispatch_region.yaml +64 -0
- aemo_mcp-0.1.2/src/aemo_mcp/data/curated/generation_scada.yaml +75 -0
- aemo_mcp-0.1.2/src/aemo_mcp/data/curated/interconnector_flows.yaml +61 -0
- aemo_mcp-0.1.2/src/aemo_mcp/data/curated/predispatch_30min.yaml +65 -0
- aemo_mcp-0.1.2/src/aemo_mcp/data/curated/rooftop_pv.yaml +66 -0
- aemo_mcp-0.1.2/src/aemo_mcp/data/duid_snapshot.csv +357 -0
- aemo_mcp-0.1.2/src/aemo_mcp/duid_lookup.py +138 -0
- aemo_mcp-0.1.2/src/aemo_mcp/feeds.py +153 -0
- aemo_mcp-0.1.2/src/aemo_mcp/fetch.py +598 -0
- aemo_mcp-0.1.2/src/aemo_mcp/models.py +99 -0
- aemo_mcp-0.1.2/src/aemo_mcp/parsing.py +138 -0
- aemo_mcp-0.1.2/src/aemo_mcp/py.typed +0 -0
- aemo_mcp-0.1.2/src/aemo_mcp/server.py +591 -0
- aemo_mcp-0.1.2/src/aemo_mcp/shaping.py +249 -0
- aemo_mcp-0.1.2/tests/__init__.py +0 -0
- aemo_mcp-0.1.2/tests/conftest.py +155 -0
- aemo_mcp-0.1.2/tests/test_cache.py +118 -0
- aemo_mcp-0.1.2/tests/test_client.py +357 -0
- aemo_mcp-0.1.2/tests/test_curated.py +202 -0
- aemo_mcp-0.1.2/tests/test_duid_lookup.py +142 -0
- aemo_mcp-0.1.2/tests/test_edge_cases.py +283 -0
- aemo_mcp-0.1.2/tests/test_feeds.py +124 -0
- aemo_mcp-0.1.2/tests/test_fetch.py +381 -0
- aemo_mcp-0.1.2/tests/test_integration.py +201 -0
- aemo_mcp-0.1.2/tests/test_live.py +92 -0
- aemo_mcp-0.1.2/tests/test_mcp_protocol.py +78 -0
- aemo_mcp-0.1.2/tests/test_parsing.py +211 -0
- aemo_mcp-0.1.2/tests/test_regressions.py +403 -0
- aemo_mcp-0.1.2/tests/test_server_validation.py +271 -0
- aemo_mcp-0.1.2/tests/test_shaping.py +383 -0
- aemo_mcp-0.1.2/uv.lock +1547 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
|
|
3
|
+
updates:
|
|
4
|
+
# Python dependencies (pyproject.toml + uv.lock)
|
|
5
|
+
- package-ecosystem: pip
|
|
6
|
+
directory: "/"
|
|
7
|
+
schedule:
|
|
8
|
+
interval: weekly
|
|
9
|
+
day: monday
|
|
10
|
+
time: "10:00"
|
|
11
|
+
timezone: Australia/Sydney
|
|
12
|
+
open-pull-requests-limit: 5
|
|
13
|
+
groups:
|
|
14
|
+
# Group all minor + patch updates into one PR per week.
|
|
15
|
+
python-runtime-minor-patch:
|
|
16
|
+
applies-to: version-updates
|
|
17
|
+
update-types: [minor, patch]
|
|
18
|
+
labels: [dependencies]
|
|
19
|
+
commit-message:
|
|
20
|
+
prefix: deps
|
|
21
|
+
include: scope
|
|
22
|
+
|
|
23
|
+
# GitHub Actions used in workflows
|
|
24
|
+
- package-ecosystem: github-actions
|
|
25
|
+
directory: "/"
|
|
26
|
+
schedule:
|
|
27
|
+
interval: weekly
|
|
28
|
+
day: monday
|
|
29
|
+
time: "10:00"
|
|
30
|
+
timezone: Australia/Sydney
|
|
31
|
+
open-pull-requests-limit: 3
|
|
32
|
+
groups:
|
|
33
|
+
gha-minor-patch:
|
|
34
|
+
applies-to: version-updates
|
|
35
|
+
update-types: [minor, patch]
|
|
36
|
+
labels: [dependencies, github-actions]
|
|
37
|
+
commit-message:
|
|
38
|
+
prefix: ci
|
|
39
|
+
include: scope
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: CodeQL
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
schedule:
|
|
9
|
+
- cron: "21 6 * * 1"
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
analyze:
|
|
13
|
+
name: Analyze
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
permissions:
|
|
16
|
+
actions: read
|
|
17
|
+
contents: read
|
|
18
|
+
security-events: write
|
|
19
|
+
strategy:
|
|
20
|
+
fail-fast: false
|
|
21
|
+
matrix:
|
|
22
|
+
language: ["python"]
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
- name: Initialize CodeQL
|
|
26
|
+
uses: github/codeql-action/init@v3
|
|
27
|
+
with:
|
|
28
|
+
languages: ${{ matrix.language }}
|
|
29
|
+
- name: Perform CodeQL Analysis
|
|
30
|
+
uses: github/codeql-action/analyze@v3
|
|
31
|
+
with:
|
|
32
|
+
category: "/language:${{ matrix.language }}"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
# Push a tag like v0.1.0 → wheel is built and published to PyPI via
|
|
4
|
+
# Trusted Publishing (no API token in repo secrets). One-time setup:
|
|
5
|
+
# 1. https://pypi.org/manage/project/aemo-mcp/settings/publishing/
|
|
6
|
+
# 2. Add a "pending publisher":
|
|
7
|
+
# owner = Bigred97
|
|
8
|
+
# repo = aemo-mcp
|
|
9
|
+
# workflow = release.yml
|
|
10
|
+
# environment = pypi
|
|
11
|
+
# 3. Create a `pypi` environment in repo Settings → Environments
|
|
12
|
+
# (no secrets needed; the environment scopes the OIDC token).
|
|
13
|
+
# Then any `git push origin vX.Y.Z` triggers this job.
|
|
14
|
+
|
|
15
|
+
on:
|
|
16
|
+
push:
|
|
17
|
+
tags:
|
|
18
|
+
- "v*"
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
release:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
environment:
|
|
24
|
+
name: pypi
|
|
25
|
+
url: https://pypi.org/project/aemo-mcp/
|
|
26
|
+
permissions:
|
|
27
|
+
id-token: write # required for Trusted Publishing OIDC
|
|
28
|
+
contents: read
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v4
|
|
31
|
+
|
|
32
|
+
- name: Install uv
|
|
33
|
+
uses: astral-sh/setup-uv@v3
|
|
34
|
+
|
|
35
|
+
- name: Sanity-check the tag matches pyproject.toml
|
|
36
|
+
run: |
|
|
37
|
+
TAG_VERSION="${GITHUB_REF_NAME#v}"
|
|
38
|
+
PYPROJECT_VERSION=$(uv run python -c "import tomllib,sys; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
39
|
+
if [ "$TAG_VERSION" != "$PYPROJECT_VERSION" ]; then
|
|
40
|
+
echo "::error::Tag $GITHUB_REF_NAME does not match pyproject version $PYPROJECT_VERSION"
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
echo "Releasing version $PYPROJECT_VERSION"
|
|
44
|
+
|
|
45
|
+
- name: Build wheel + sdist
|
|
46
|
+
run: uv build
|
|
47
|
+
|
|
48
|
+
- name: Publish to PyPI (Trusted Publishing)
|
|
49
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
50
|
+
|
|
51
|
+
- uses: actions/upload-artifact@v4
|
|
52
|
+
with:
|
|
53
|
+
name: dist
|
|
54
|
+
path: dist/
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- name: Install uv
|
|
19
|
+
uses: astral-sh/setup-uv@v3
|
|
20
|
+
with:
|
|
21
|
+
enable-cache: true
|
|
22
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
23
|
+
run: uv python install ${{ matrix.python-version }}
|
|
24
|
+
- name: Sync dependencies
|
|
25
|
+
run: uv sync --extra dev
|
|
26
|
+
- name: Install package
|
|
27
|
+
run: uv pip install -e .
|
|
28
|
+
- name: Run unit tests
|
|
29
|
+
run: uv run pytest -q
|
|
30
|
+
|
|
31
|
+
build:
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
needs: test
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v4
|
|
36
|
+
- name: Install uv
|
|
37
|
+
uses: astral-sh/setup-uv@v3
|
|
38
|
+
- name: Build wheel + sdist
|
|
39
|
+
run: uv build
|
|
40
|
+
- name: Verify wheel installs cleanly
|
|
41
|
+
run: |
|
|
42
|
+
uv run --isolated --with ./dist/*.whl python -c \
|
|
43
|
+
"import aemo_mcp.server as s; n = len(s.list_curated()); assert n >= 7, f'expected >=7 curated, got {n}'; print(f'OK ({n} curated feeds)')"
|
|
44
|
+
- uses: actions/upload-artifact@v4
|
|
45
|
+
with:
|
|
46
|
+
name: dist
|
|
47
|
+
path: dist/
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*$py.class
|
|
4
|
+
.Python
|
|
5
|
+
.venv/
|
|
6
|
+
venv/
|
|
7
|
+
env/
|
|
8
|
+
|
|
9
|
+
build/
|
|
10
|
+
dist/
|
|
11
|
+
*.egg-info/
|
|
12
|
+
*.egg
|
|
13
|
+
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.coverage
|
|
16
|
+
htmlcov/
|
|
17
|
+
.tox/
|
|
18
|
+
|
|
19
|
+
.DS_Store
|
|
20
|
+
.idea/
|
|
21
|
+
.vscode/
|
|
22
|
+
|
|
23
|
+
# Local cache database
|
|
24
|
+
*.db
|
|
25
|
+
*.db-journal
|
|
26
|
+
*.db-wal
|
|
27
|
+
*.db-shm
|
|
28
|
+
|
|
29
|
+
# uv lockfile pinned in repo
|
|
30
|
+
# uv.lock is committed deliberately
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.2 (2026-05-15)
|
|
4
|
+
|
|
5
|
+
Portfolio parity — stale-cache fallback + error-message sweep + dependabot +
|
|
6
|
+
CLAUDE.md. 297 unit tests (was 288) + 10 live tests, 3× zero-flake green.
|
|
7
|
+
|
|
8
|
+
- **Add: stale-cache fallback (graceful degradation).** When NEMWEB returns
|
|
9
|
+
5xx or is unreachable (`httpx.RequestError`), `AEMOClient._fetch_cached`
|
|
10
|
+
now falls back to the most-recent cached payload (regardless of TTL) via
|
|
11
|
+
the new `Cache.get_stale()` and records the staleness on a `_stale_signal`
|
|
12
|
+
ContextVar. Server-side tool wrappers (`get_data`, `latest`) read the
|
|
13
|
+
signal after the fetch chain and surface it on the response via
|
|
14
|
+
`DataResponse.stale=True, stale_reason="AEMO/OpenNEM fetch returned X
|
|
15
|
+
for Y; serving cached payload from ~N minute(s) ago"`. Mirrors abs-mcp
|
|
16
|
+
0.2.13 / rba-mcp 0.1.10 patterns. Empty-cache fallback preserves the
|
|
17
|
+
original `AEMOAPIError` behaviour.
|
|
18
|
+
- **Add: `DataResponse.stale_reason` and `truncated_at` fields.** Aligns the
|
|
19
|
+
envelope with the rest of the portfolio (abs / rba / ato / apra / aihw /
|
|
20
|
+
asic). `stale` retains its dual meaning: True when the latest NEM
|
|
21
|
+
observation is older than 2x cadence OR a cached fallback was served.
|
|
22
|
+
- **Error-message sweep.** Every weak `ValueError` rewritten to suggest the
|
|
23
|
+
correction via stdlib `difflib.get_close_matches`. Unknown dataset IDs
|
|
24
|
+
now emit `Did you mean 'dispatch_price'?` for close typos; unknown filter
|
|
25
|
+
keys emit `Did you mean 'region'?`; unknown formats emit `Did you mean
|
|
26
|
+
'records'?`. Period errors now show a worked example
|
|
27
|
+
(`'2026-05-14' or '2026-05-14 09:00'`). `Unsupported aggregation
|
|
28
|
+
dimension` in duid_lookup now lists valid options. No new top-level
|
|
29
|
+
dependencies.
|
|
30
|
+
- **Add: `CLAUDE.md`.** Repo-specific conventions auto-loaded by Claude
|
|
31
|
+
Code, mirroring the rest of the portfolio. Calls out the AEMO-specific
|
|
32
|
+
module set (`fetch.py`, `feeds.py`, `parsing.py`, `duid_lookup.py`),
|
|
33
|
+
the dual-meaning `stale` flag, the 5-min cadence cache-TTL ladder, and
|
|
34
|
+
the `/Reports/Current/` vs `/Reports/Archive/` pivot.
|
|
35
|
+
- **Add: `.github/dependabot.yml`.** Weekly minor + patch update PRs for
|
|
36
|
+
pip + GitHub Actions, grouped, Mon 10:00 Sydney. Verbatim from the
|
|
37
|
+
sister repos.
|
|
38
|
+
- **Tests: +4 stale-fallback regressions + 5 error-message-suggestion
|
|
39
|
+
regressions.** New tests cover: 5xx + stale cache → fallback + signal;
|
|
40
|
+
ConnectError + stale cache → same; 5xx + empty cache → still raises
|
|
41
|
+
`AEMOAPIError`; `Cache.get_stale()` round-trip + TTL bypass; "Did you
|
|
42
|
+
mean" hint for dataset / filter-key / format typos; period worked
|
|
43
|
+
example.
|
|
44
|
+
|
|
45
|
+
## 0.1.1 (2026-05-15)
|
|
46
|
+
|
|
47
|
+
Customer-simulation hardening pass. Hammered every dataset against the live
|
|
48
|
+
NEMWEB feed; fixed every bug surfaced. 288 unit tests (was 225) + 10 live
|
|
49
|
+
tests, 10× zero-flake green.
|
|
50
|
+
|
|
51
|
+
- **Fix: `daily_summary` section name** — Daily_Reports publishes regional
|
|
52
|
+
data under `DREGION.` (with trailing dot — second cell of I-row is empty),
|
|
53
|
+
not `DISPATCH.PRICE`. YAML updated + 4 metrics (rrp, total_demand,
|
|
54
|
+
dispatchable_generation, net_interchange) now resolve correctly. The
|
|
55
|
+
parser already builds the empty-subname name; added a regression test.
|
|
56
|
+
- **Fix: `predispatch_30min` section names** — the actual NEMWEB sections
|
|
57
|
+
are `PREDISPATCH.REGION_SOLUTION` (demand/generation) and
|
|
58
|
+
`PREDISPATCH.REGION_PRICES` (RRP); the YAML pointed at the non-existent
|
|
59
|
+
`PREDISPATCH.REGIONSUM`. Forward curves now return 135+ records per
|
|
60
|
+
region per run (instead of 1).
|
|
61
|
+
- **Fix: `rooftop_pv` filename regex** — AEMO renamed the ACTUAL infix
|
|
62
|
+
from `MEASUREMENT` to `SATELLITE` and now also publishes some files
|
|
63
|
+
with no infix at all. Regex accepts all three forms.
|
|
64
|
+
- **Fix: archive fallback** — added `/Reports/Archive/<feed>/` (one ZIP-of-
|
|
65
|
+
ZIPs per day) fetch for windows older than 4 hours. Daily archive zips
|
|
66
|
+
are unpacked recursively. Demo 3 ("did SA hit negative pricing in the
|
|
67
|
+
last 24h?") and Demo 4 ("weekly avg dispatch price for VIC, last 4
|
|
68
|
+
weeks") now succeed where they previously hit 403 on rolled-out
|
|
69
|
+
/Current/ files. Capped at 31 days per response.
|
|
70
|
+
- **Fix: archive path** — the path constructor was including the literal
|
|
71
|
+
`Current` segment, producing `/Reports/Archive/Current/...` instead of
|
|
72
|
+
`/Reports/Archive/...`. Now strips correctly.
|
|
73
|
+
- **Fix: 5-min feeds: skip rolled-out files instead of failing the whole
|
|
74
|
+
response** — NEMWEB rolls files in/out of /Current/ continuously; a
|
|
75
|
+
file present in the directory listing may have moved to /Archive/ by
|
|
76
|
+
the time we GET it. Individual 403/404s now skip silently; only a
|
|
77
|
+
fully-empty result surfaces an error.
|
|
78
|
+
- **Fix: `latest()` on forecast feeds returns the full forward curve**,
|
|
79
|
+
not a single row collapsed to the furthest-out horizon. `rooftop_pv`
|
|
80
|
+
forecast and `predispatch_30min` now behave correctly.
|
|
81
|
+
- **Fix: section filter at folder level** — `filters={"section": "actual"}`
|
|
82
|
+
now skips fetching the FORECAST folder entirely, cutting one HTTP round
|
|
83
|
+
trip + sidestepping flaky listings.
|
|
84
|
+
- **Fix: section filter row-level skip** — the synthesised `section`
|
|
85
|
+
filter is no longer treated as a row column (which would reject every
|
|
86
|
+
row since rows have no SECTION cell).
|
|
87
|
+
- **Fix: section dedup** — when AEMO emits the same section twice in one
|
|
88
|
+
file with different versions (e.g. `DREGION.` v2 + v3 in Daily_Reports),
|
|
89
|
+
we now combine and dedupe by (settlement_column, filter columns)
|
|
90
|
+
instead of taking only the first match.
|
|
91
|
+
- **Fix: `Cache(db_path=DEFAULT_DB_PATH)` honors monkeypatches** — the
|
|
92
|
+
default value was captured at class-definition time, so test
|
|
93
|
+
monkeypatches of `DEFAULT_DB_PATH` had no effect. Now resolved at
|
|
94
|
+
construction time. Fixes flaky integration test where live NEMWEB data
|
|
95
|
+
bled through respx mocks.
|
|
96
|
+
- **Fix: in-flight dedup future exception leaks** — failed fetches no
|
|
97
|
+
longer log "Future exception was never retrieved" warnings.
|
|
98
|
+
- **Expand: DUID snapshot from 128 → 350 entries**, covering the majority
|
|
99
|
+
of active NEM units across all 5 regions and 7 fuel buckets. Generation-
|
|
100
|
+
by-fuel queries (QLD gas, NSW solar, SA battery, etc.) now return non-
|
|
101
|
+
empty results.
|
|
102
|
+
- **Tests: +63 regressions + edge cases.** Tests now cover: every bug above
|
|
103
|
+
+ unicode queries, very long queries, special-char queries, negative
|
|
104
|
+
TTL, concurrent 10x dedup, DOS line endings, truncated CSV, quoted
|
|
105
|
+
commas, ZIP-bomb defence, every-dataset describe sweep, fuzzy ranker
|
|
106
|
+
invariants, and DUID coverage thresholds per fuel/region.
|
|
107
|
+
|
|
108
|
+
## 0.1.0 (2026-05-14)
|
|
109
|
+
|
|
110
|
+
Initial release. MCP server wrapping AEMO NEMWEB feeds with 5 plain-English
|
|
111
|
+
tools and 7 curated datasets.
|
|
112
|
+
|
|
113
|
+
- **5 tools** mirroring abs-mcp / rba-mcp / ato-mcp: `search_datasets`,
|
|
114
|
+
`describe_dataset`, `get_data`, `latest`, `list_curated`.
|
|
115
|
+
- **7 curated feeds** covering ~95% of typical NEM analytic queries:
|
|
116
|
+
- `dispatch_price` — 5-min regional spot price (RRP) per NEM region
|
|
117
|
+
- `dispatch_region` — 5-min total demand, scheduled + semi-scheduled generation, net interchange
|
|
118
|
+
- `interconnector_flows` — 5-min MW flow across the 6 NEM interconnectors
|
|
119
|
+
- `generation_scada` — 5-min DUID-level SCADA MW (every generating unit)
|
|
120
|
+
- `rooftop_pv` — 30-min regional rooftop solar (actual + forecast)
|
|
121
|
+
- `predispatch_30min` — 30-min half-hourly forecast, ~40h horizon
|
|
122
|
+
- `daily_summary` — daily rolled-up compendium of yesterday's price + demand + dispatch
|
|
123
|
+
- **Trust contract** on every `DataResponse`: `source`, `attribution`,
|
|
124
|
+
`source_url`, `retrieved_at`, `interval_start`, `interval_end`, `stale`.
|
|
125
|
+
- **Live-fetch only**, no pre-bundled NEMWEB archives in the wheel.
|
|
126
|
+
- **Cache TTLs tuned per cadence**: 60s for 5-min feeds, 5min for 30-min
|
|
127
|
+
feeds, 1h forecasts, 24h daily, immutable for archived timestamped files.
|
|
128
|
+
- **In-flight request deduplication** — concurrent callers share one HTTP
|
|
129
|
+
request per URL (critical at 5-min cadence with many users).
|
|
130
|
+
- **AEMO Copyright Permissions** attribution string in every response.
|
|
131
|
+
|
|
132
|
+
Licence: AEMO grants general permission to use AEMO Material for any purpose
|
|
133
|
+
with attribution. See https://aemo.com.au/privacy-and-legal-notices/copyright-permissions.
|
aemo_mcp-0.1.2/CLAUDE.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# aemo-mcp
|
|
2
|
+
|
|
3
|
+
Sister MCP in the Australian Public Data stack. See `../CLAUDE.md` for
|
|
4
|
+
portfolio-wide conventions; this file captures repo-specific details
|
|
5
|
+
plus the cross-sister discipline.
|
|
6
|
+
|
|
7
|
+
## Source
|
|
8
|
+
|
|
9
|
+
| | |
|
|
10
|
+
|--|--|
|
|
11
|
+
| Source agency | Australian Energy Market Operator (AEMO) |
|
|
12
|
+
| Source URL | http://nemweb.com.au/Reports/Current/ |
|
|
13
|
+
| Data format | Multi-section CSV (AEMO `C,/I,/D,` rows) packed in ZIP files, served from NEMWEB (IIS static file server). Directory listings are HTML. |
|
|
14
|
+
| Licence | AEMO Copyright Permissions (general permission for any purpose with attribution; commercial use allowed) |
|
|
15
|
+
| Licence URL | https://aemo.com.au/privacy-and-legal-notices/copyright-permissions |
|
|
16
|
+
| Python module | `aemo_mcp` |
|
|
17
|
+
| PyPI package | `aemo-mcp` |
|
|
18
|
+
| GitHub | https://github.com/Bigred97/aemo-mcp |
|
|
19
|
+
|
|
20
|
+
Note: AEMO's data is NOT CC-BY. Their Copyright Permissions policy is similar
|
|
21
|
+
in effect (general permission with attribution) but the canonical attribution
|
|
22
|
+
string differs — see `models._AEMO_ATTRIBUTION`.
|
|
23
|
+
|
|
24
|
+
## Curated datasets (7)
|
|
25
|
+
|
|
26
|
+
dispatch_price · dispatch_region · interconnector_flows · generation_scada ·
|
|
27
|
+
rooftop_pv · predispatch_30min · daily_summary
|
|
28
|
+
|
|
29
|
+
All cover the NEM (NSW1, QLD1, SA1, TAS1, VIC1). Western Australia (WEM) and
|
|
30
|
+
the Northern Territory are not on the NEM and are out of scope.
|
|
31
|
+
|
|
32
|
+
## Repo-specific module set
|
|
33
|
+
|
|
34
|
+
Required (every sister): `server.py`, `models.py`, `curated.py`, `client.py`, `cache.py`, `shaping.py`, `data/curated/*.yaml`
|
|
35
|
+
|
|
36
|
+
Repo-specific extras:
|
|
37
|
+
- `fetch.py` — orchestration layer between server.py and the HTTP/parsing
|
|
38
|
+
stack. Resolves curated dataset → folder(s) → file selection (Current vs
|
|
39
|
+
Archive) → ZIP fetch → CSV parse → filter → shape. Sits where most
|
|
40
|
+
sisters' `server.py` would have inline orchestration logic — pulled out
|
|
41
|
+
because NEM file selection (5-min vs 30-min, Current vs Archive, latest
|
|
42
|
+
vs window, forecast vs actual) is non-trivial.
|
|
43
|
+
- `feeds.py` — dataset search ranking + DatasetSummary projection. Replaces
|
|
44
|
+
rba-mcp's `tables.py` / abs-mcp's `catalog.py`.
|
|
45
|
+
- `parsing.py` — AEMO multi-section CSV parser + ZIP unpacker. Each NEMWEB
|
|
46
|
+
ZIP holds one CSV with one or more `I,/D,` sections (DISPATCH.PRICE,
|
|
47
|
+
DISPATCH.REGIONSUM, DISPATCH.INTERCONNECTORRES, ...). Stdlib `csv` +
|
|
48
|
+
`zipfile` only; no pandas.
|
|
49
|
+
- `duid_lookup.py` — DUID → region/fuel join table for `generation_scada`.
|
|
50
|
+
Static snapshot in `data/duid_snapshot.csv` (DUIDs change infrequently);
|
|
51
|
+
refreshed periodically. Used to translate `region`/`fuel` filters into a
|
|
52
|
+
DUID allow-set before filtering DISPATCH.UNIT_SCADA rows.
|
|
53
|
+
|
|
54
|
+
## Repo-specific gotchas
|
|
55
|
+
|
|
56
|
+
- **5-min cadence drives cache TTLs.** `live` = 60s, `half_hour` = 5min,
|
|
57
|
+
`forecast` = 1h, `daily` = 24h, `archive` = 7d, `listing` = 30s. Timestamped
|
|
58
|
+
NEMWEB files are immutable once written (filename embeds the interval), so
|
|
59
|
+
the file-body cache is effectively infinite. Only the directory listing
|
|
60
|
+
has freshness sensitivity.
|
|
61
|
+
- **AEMO market time is UTC+10, no DST.** NEM is Brisbane-aligned year-round.
|
|
62
|
+
All NEMWEB timestamps in this code are tz-aware in NEM time (`NEM_TZ`).
|
|
63
|
+
- **`/Reports/Current/` vs `/Reports/Archive/`.** Current holds ~24-48h of
|
|
64
|
+
5-min files; older intervals roll into daily ZIP-of-ZIPs compendia at
|
|
65
|
+
`/Reports/Archive/<feed>/PUBLIC_<feed>_YYYYMMDD.zip`. `fetch.py` auto-pivots
|
|
66
|
+
to Archive for windows older than `_CURRENT_WINDOW_HOURS` (4h). Archive
|
|
67
|
+
fallback unpacks two ZIP levels.
|
|
68
|
+
- **In-flight request deduplication is mandatory.** At 5-min cadence with
|
|
69
|
+
many concurrent users, naive caching would hammer NEMWEB. `AEMOClient._in_flight`
|
|
70
|
+
shares one HTTP call across concurrent identical URLs.
|
|
71
|
+
- **Latest-file detection is purely lexicographic.** AEMO embeds the interval
|
|
72
|
+
timestamp (`YYYYMMDDHHmm`) as the first 12-digit group in every filename,
|
|
73
|
+
so `max(filenames)` is the most recent. No HEAD requests needed.
|
|
74
|
+
- **Forecast feeds use `latest()` differently.** For `rooftop_pv` forecast and
|
|
75
|
+
`predispatch_30min`, `latest()` returns the FULL forward curve from the
|
|
76
|
+
most-recent run, not a single collapsed row per dim. `_is_forecast_folder`
|
|
77
|
+
controls this.
|
|
78
|
+
- **AEMO `C,/I,/D,` CSV format.** `C` = comment, `I` = schema row (opens a
|
|
79
|
+
new section), `D` = data row. Section name is `col1.col2` of the I-row;
|
|
80
|
+
data rows positionally map cells 4+ to the I-row's column names. One ZIP
|
|
81
|
+
can hold many sections, and one file can hold the same section twice in
|
|
82
|
+
two schema versions (`DREGION.` v2 + v3 in Daily_Reports) — `find_sections`
|
|
83
|
+
returns all; the caller dedupes.
|
|
84
|
+
- **NEMWEB rolls files in/out continuously.** A filename present in the
|
|
85
|
+
directory listing may have moved to `/Archive/` between the listing GET
|
|
86
|
+
and the per-file GET. Individual 403/404 must NOT fail the whole response
|
|
87
|
+
— `_fetch_current_zips` skips and continues.
|
|
88
|
+
- **`stale` field has dual meaning.** Set True if EITHER the latest observation
|
|
89
|
+
is older than 2× the feed cadence (NEM-side delay) OR a cached-fallback was
|
|
90
|
+
served because NEMWEB returned a non-2xx (graceful degradation). `stale_reason`
|
|
91
|
+
disambiguates.
|
|
92
|
+
|
|
93
|
+
## Cache kinds (aemo-specific, not portable to other sisters)
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
live 60s — 5-min dispatch feeds
|
|
97
|
+
half_hour 5min — 30-min feeds: rooftop PV actual, predispatch
|
|
98
|
+
forecast 1h — longer-horizon forecast bundles
|
|
99
|
+
daily 24h — daily rolled-up archives
|
|
100
|
+
archive 7d — immutable historical files (could be infinite)
|
|
101
|
+
listing 30s — NEMWEB directory HTML
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## The 5-tool surface (uniform across sisters — non-negotiable)
|
|
107
|
+
|
|
108
|
+
1. `search_datasets(query, limit)` — fuzzy-search the 7 curated NEM feeds
|
|
109
|
+
2. `describe_dataset(dataset_id)` — schema + filters + cadence + source URL
|
|
110
|
+
3. `get_data(dataset_id, filters, start_period, end_period, format)` — query
|
|
111
|
+
4. `latest(dataset_id, filters)` — most recent 5-min / 30-min / daily interval
|
|
112
|
+
5. `list_curated()` — enumerate supported IDs
|
|
113
|
+
|
|
114
|
+
Every parameter uses `Annotated[Type, Field(description=..., examples=[...])]`.
|
|
115
|
+
This is the Glama Tool Definition Quality requirement — non-negotiable.
|
|
116
|
+
|
|
117
|
+
## Trust contract (every DataResponse carries)
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
source "Australian Energy Market Operator"
|
|
121
|
+
source_url the NEMWEB folder the data came from
|
|
122
|
+
attribution full AEMO Copyright Permissions attribution string
|
|
123
|
+
retrieved_at UTC timestamp
|
|
124
|
+
server_version importlib.metadata.version("aemo-mcp")
|
|
125
|
+
interval_start ISO-8601 in AEMO market time (UTC+10)
|
|
126
|
+
interval_end ISO-8601 in AEMO market time (UTC+10)
|
|
127
|
+
stale True when the feed is delayed OR cached fallback was served
|
|
128
|
+
stale_reason human-readable when stale=True (e.g. "AEMO/OpenNEM fetch returned 503 ...")
|
|
129
|
+
truncated_at int | None — set when latest() caps a large response
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## The 5 quality dimensions (audit every release against these)
|
|
133
|
+
|
|
134
|
+
1. **Semantic Clarity** — verb-noun tool names, Annotated[Field] with examples, rich docstrings (Examples + Returns blocks), `pattern=` constraints on dataset IDs and region codes
|
|
135
|
+
2. **Data Pruning** — <10k tokens for typical responses, `latest()` returns the most-recent interval(s) for the filter dims rather than the whole file, no leaked AEMO row metadata in observations
|
|
136
|
+
3. **Cross-Agency Joining** — AEMO market time uniformly UTC+10; region codes (NSW1/QLD1/SA1/TAS1/VIC1) match the canonical AEMO IDs that other sisters can join against; periods accept the shared YYYY / YYYY-MM / YYYY-MM-DD / YYYY-MM-DD HH:MM grammar
|
|
137
|
+
4. **Reliability + Caching** — TTLs tuned per AEMO cadence (60s / 5min / 1h / 24h / 7d), self-heal on `sqlite3.DatabaseError`, **graceful degradation**: when NEMWEB returns 5xx or is unreachable, fall back to last cached payload via `Cache.get_stale()` and set `stale=True, stale_reason="..."` rather than raising
|
|
138
|
+
5. **Deterministic Error Handling** — every `ValueError` carries a "Try X" / "Did you mean X?" / "Valid options: ..." hint that suggests the correction, not just describes the rejection
|
|
139
|
+
|
|
140
|
+
## Test taxonomy
|
|
141
|
+
|
|
142
|
+
Required: `test_cache.py`, `test_curated.py`, `test_server_validation.py`, `test_shaping.py`, `test_integration.py` (live, `@pytest.mark.live`)
|
|
143
|
+
Recommended: `test_client.py`, `test_fetch.py`, `test_feeds.py`, `test_parsing.py`, `test_duid_lookup.py`, `test_mcp_protocol.py`, `test_regressions.py`, `test_edge_cases.py`, `test_live.py`
|
|
144
|
+
|
|
145
|
+
Zero-flake bar: full unit suite must run 10× consecutively green before tagging a release.
|
|
146
|
+
|
|
147
|
+
## Release workflow (Trusted Publishing via OIDC, no API tokens in CI)
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
1. Bump version in pyproject.toml (semver)
|
|
151
|
+
2. Update CHANGELOG.md (latest entry at top, semver headings)
|
|
152
|
+
3. uv run pytest × 10 — zero flakes
|
|
153
|
+
4. git commit -am "X.Y.Z: <one-line reason>"
|
|
154
|
+
5. git tag -a vX.Y.Z -m "X.Y.Z: <reason>"
|
|
155
|
+
6. git push origin main vX.Y.Z
|
|
156
|
+
7. release.yml fires → builds → OIDC publish → PyPI
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
PyPI new-project rate limit: 5/day per account; not an issue for existing
|
|
160
|
+
projects (only counts NEW package names). `aemo-mcp` is already published.
|
|
161
|
+
|
|
162
|
+
## Anti-patterns — DO NOT do these
|
|
163
|
+
|
|
164
|
+
- Don't add a 6th tool; uniform 5-tool surface is the brand
|
|
165
|
+
- Don't add new top-level dependencies beyond what other sisters use (httpx, pydantic, fastmcp, aiosqlite, rapidfuzz, pyyaml)
|
|
166
|
+
- Don't introduce pandas at the parsing layer — stdlib `csv` + `zipfile` is enough
|
|
167
|
+
- Don't bundle large NEMWEB archives in the wheel; cache at runtime
|
|
168
|
+
- Don't ship without 10 consecutive zero-flake pytest runs
|
|
169
|
+
- Don't echo PyPI tokens / PATs in tool output, commit messages, or CHANGELOG
|
|
170
|
+
- Don't classify a slow source as a bug — NEMWEB cold fetches take 1-3s, only flag >10s or actual errors
|
|
171
|
+
- Don't hammer NEMWEB — in-flight dedup + cache are mandatory at 5-min cadence
|
|
172
|
+
- Don't widen scope mid-audit-loop; loops are fix-only
|
|
173
|
+
|
|
174
|
+
## Common operations
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
cd . # in the repo
|
|
178
|
+
uv sync --extra dev # install deps
|
|
179
|
+
uv run pytest # unit tests
|
|
180
|
+
uv run pytest -m live # live tests too
|
|
181
|
+
uvx --refresh --from aemo-mcp==<ver> python -c "..." # smoke a published wheel
|
|
182
|
+
```
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
This project adopts the [Contributor Covenant v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
|
|
4
|
+
|
|
5
|
+
## Our Pledge
|
|
6
|
+
|
|
7
|
+
We pledge to make participation in our project a harassment-free experience
|
|
8
|
+
for everyone, regardless of age, body size, visible or invisible disability,
|
|
9
|
+
ethnicity, sex characteristics, gender identity and expression, level of
|
|
10
|
+
experience, education, socio-economic status, nationality, personal appearance,
|
|
11
|
+
race, caste, color, religion, or sexual identity and orientation.
|
|
12
|
+
|
|
13
|
+
## Standards
|
|
14
|
+
|
|
15
|
+
Examples of behavior that contributes to a positive environment:
|
|
16
|
+
|
|
17
|
+
- Demonstrating empathy and kindness toward others
|
|
18
|
+
- Being respectful of differing opinions, viewpoints, and experiences
|
|
19
|
+
- Giving and gracefully accepting constructive feedback
|
|
20
|
+
- Accepting responsibility, apologizing, and learning from mistakes
|
|
21
|
+
- Focusing on what is best for the overall community
|
|
22
|
+
|
|
23
|
+
Unacceptable behavior includes:
|
|
24
|
+
|
|
25
|
+
- Trolling, insulting or derogatory comments, and personal or political attacks
|
|
26
|
+
- Public or private harassment
|
|
27
|
+
- Publishing others' private information without explicit permission
|
|
28
|
+
- Other conduct which could reasonably be considered inappropriate
|
|
29
|
+
|
|
30
|
+
## Enforcement
|
|
31
|
+
|
|
32
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
33
|
+
reported to the project maintainers via GitHub issues or a private channel.
|
|
34
|
+
All complaints will be reviewed and investigated promptly and fairly.
|
|
35
|
+
|
|
36
|
+
## Attribution
|
|
37
|
+
|
|
38
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
|
39
|
+
version 2.1. [homepage]: https://www.contributor-covenant.org
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Contributing to aemo-mcp
|
|
2
|
+
|
|
3
|
+
Thanks for the interest. This MCP server is part of a family of Australian
|
|
4
|
+
open-data wrappers ([abs-mcp](https://github.com/Bigred97/abs-mcp),
|
|
5
|
+
[rba-mcp](https://github.com/Bigred97/rba-mcp),
|
|
6
|
+
[ato-mcp](https://github.com/Bigred97/ato-mcp),
|
|
7
|
+
[apra-mcp](https://github.com/Bigred97/apra-mcp),
|
|
8
|
+
[aihw-mcp](https://github.com/Bigred97/aihw-mcp),
|
|
9
|
+
[asic-mcp](https://github.com/Bigred97/asic-mcp),
|
|
10
|
+
[au-weather-mcp](https://github.com/Bigred97/au-weather-mcp)) and follows the
|
|
11
|
+
same architectural template.
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
git clone https://github.com/Bigred97/aemo-mcp
|
|
17
|
+
cd aemo-mcp
|
|
18
|
+
uv sync --extra dev
|
|
19
|
+
uv pip install -e .
|
|
20
|
+
uv run pytest -q
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## What we accept
|
|
24
|
+
|
|
25
|
+
- **Bug fixes** with a regression test.
|
|
26
|
+
- **New curated feeds** for high-value NEMWEB reports. Add a YAML in
|
|
27
|
+
`src/aemo_mcp/data/curated/`, extend `src/aemo_mcp/data/feeds.yaml`, and
|
|
28
|
+
add at least one unit test against a fixture in `tests/fixtures/`.
|
|
29
|
+
- **Search-keyword expansions** to help LLMs route common queries — must be
|
|
30
|
+
paired with a routing regression test in `test_feeds.py`.
|
|
31
|
+
- **Performance improvements** in parsing, caching, or in-flight dedup.
|
|
32
|
+
|
|
33
|
+
## What we don't accept (by design)
|
|
34
|
+
|
|
35
|
+
- **Tool count > 5.** We hold to the standard `search_datasets / describe_dataset / get_data / latest / list_curated` surface across all sibling MCPs.
|
|
36
|
+
- **Pre-bundled NEMWEB archives in the wheel.** Live fetch only.
|
|
37
|
+
- **Pandas at the tool surface.** Records must be plain `list[dict]` / `list[Observation]`. Pandas is fine internally for CSV parsing.
|
|
38
|
+
- **Dependencies on `nemosis`, `nempy`, `openelectricity`.** Stay light.
|
|
39
|
+
|
|
40
|
+
## Tests
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Unit tests (offline)
|
|
44
|
+
uv run pytest -q
|
|
45
|
+
|
|
46
|
+
# Live tests (hit NEMWEB)
|
|
47
|
+
uv run pytest -q -m live
|
|
48
|
+
|
|
49
|
+
# 10x zero-flake validation
|
|
50
|
+
for i in $(seq 1 10); do uv run pytest -q || break; done
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Release
|
|
54
|
+
|
|
55
|
+
Tag the commit:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
git tag v0.1.x
|
|
59
|
+
git push origin v0.1.x
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
GitHub Actions builds the wheel and publishes to PyPI via Trusted Publishing
|
|
63
|
+
(OIDC, no token needed).
|
|
64
|
+
|
|
65
|
+
## Attribution
|
|
66
|
+
|
|
67
|
+
Every response carries the AEMO Copyright Permissions attribution string.
|
|
68
|
+
Keep that contract intact across changes.
|
aemo_mcp-0.1.2/LICENSE
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Harry Vass
|
|
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.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
Note on AEMO data: this software fetches data published by the Australian
|
|
26
|
+
Energy Market Operator (AEMO) via NEMWEB. AEMO grants general permission to
|
|
27
|
+
use AEMO Material for any purpose (commercial included) on the sole condition
|
|
28
|
+
of accurate attribution of the relevant material and AEMO as its author. See
|
|
29
|
+
https://aemo.com.au/privacy-and-legal-notices/copyright-permissions.
|
|
30
|
+
|
|
31
|
+
End-users redistributing data fetched via this server must credit AEMO. The
|
|
32
|
+
`DataResponse.attribution` field on every response carries the canonical
|
|
33
|
+
attribution string.
|