OpenEnergyPlatform / OpenEnergyPlatform/oeplatform
Replace the whole bundle in one call
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 65
- Forks
- 29
- Avg merge
- 15h 25m
- Merged PRs (30d)
- 32
Description
Description of the issue
The twelfth and last of the OEKG REST API slices, from the wayfinder map's tickets note
("Replace the whole bundle in one call"), and the one the whole effort started from: a
modelling pipeline declares what the bundle should look like and the server makes it so.
Eleven slices are on develop (#2434, #2439, #2442, #2443, #2446, #2448, #2451, #2455,
#2467, #2471, #2468). This one collects the three that landed on 2026-09-15 — Delete a whole
bundle, in two steps (#2467), The read side, completed (#2471) and Say whether a linked
dataset is still there (#2468).
What to build
A separately named endpoint — POST /api/v0/scenario-bundles/<uid>/replace/ — taking the
complete desired bundle including nested sub-resources, applying the difference as one atomic
update.
Full replacement is deliberately not offered on the bundle's own URL. PUT was rejected
early for accident-proneness rather than for its semantics, and an endpoint you have to name
and guard is not hit by accident. Single-value edits stay on PATCH; the full payload is the
price of the declarative endpoint alone.
Delete-by-omission is the semantics of this one endpoint and exists nowhere else in the
API. Anything in the graph and absent from the payload is removed through the typed
containment walk of #2455, so shared nodes are still only unlinked, never deleted.
Acceptance criteria
-
POST .../<uid>/replace/takes the complete desired bundle, nested sub-resources included. - It requires
If-Match, with the same refusal order every other write uses: existence,
ownership, precondition.428absent,412stale,409when the bound guard fails. - Omission removes, through
removal.plan_removal's walk — so a node another bundle cites
is unlinked rather than deleted, and the guard can still only downgrade. - The response reports what was removed and what was unlinked, so a client asserts on
the consequence rather than inferring it. - A nested sub-resource carrying an identifier is matched to the existing one; without an
identifier it is created. Without this, a re-import deletes and recreates every scenario
on every run — churning identifiers and filling the history with false deletions. - The whole operation is one request to the store and produces one history entry
with a real diff. Never a delete followed by a create. - The full pipeline story works end to end: look up by acronym (
GET ?acronym=, #2471);
create if absent; otherwise replace. The client holds no state between runs. - Changelog entry.
From #2473 — the dataset-link half, decided but not built
- Dataset links nest under each scenario on the bundle read and on the nested bundle
create, so a bundle read is round-trippable. Today they are in neither
(_representandbuild_bundle_graphboth loopBUNDLE_PARTS, and a link is not a
part) while their classes are inremoval.BUNDLE_LOCAL_CLASSES— which is exactly
the combination that makes a naive replace destroy every citation in the bundle. - A fourth reference kind,
ref: "external", carries an address that is not a page on
this platform. No shape change:refis never stored, only the URL is, in
oeo:has_iri. - The duplicate check compares an external link by its URL, not by
(type, ref, name).
_already_linked(oekg/dataset_link_views.py:284) compares on the three answers
deliberately, because the stored URL carries whichever host the request arrived on — for
an external link that reasoning inverts, since the URL is the only thing identifying the
target and is not derived from the name. - A replace of a bundle carrying dataset links does not destroy them. This is the test
that would not have been written by accident: the happy-path replace anybody would write
— add a scenario, change a field, remove a scenario — passes while citations are lost.
Also from the documentation track
- Rule 3 of the client rules page (#2460) gets its pinning test here. #2460 ships with
eight of nine rules and names rule 3 as pending, so this slice closes that gap —
#2462 states the obligation and cannot discharge it.
Tests
Beyond the dataset-link test above: a replace that adds, changes and removes in one call; the
removal report; matching by identifier does not churn; a stale precondition is refused; running
the same replace twice is a no-op the second time; the endpoint does not exist on the
bundle's own URL.
Same seam as every slice before it — HTTP through Django's test client over the API's named
URLs, against a real Fuseki, skipping cleanly without one (OekgGraphTestCase).
Blocked by
- #2473 — Replace must not delete dataset links by omission. Decided (see its comment of
2026-09-15); the implementation of that decision is in the criteria above.
Unblocks
- #2464 — Academy notebook: re-import a scenario bundle from a pipeline, whose whole subject
is this endpoint — and through it #2465.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the API's named URLs and the Django test client using OekgGraphTestCase against Fuseki. Read _represent, build_bundle_graph, removal.plan_removal, and _already_linked in oekg/dataset_link_views.py, then trace the existing write precondition order. Done means the named replace endpoint performs one atomic, round-trippable update with removal and unlink reports, preserves dataset links, and passes the listed end-to-end tests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- django, python
- Domain
- api, backend, databases, documentation, testing
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100