OpenEnergyPlatform / OpenEnergyPlatform/oeplatform

Replace the whole bundle in one call

Open
#2,477 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

OEKG-API
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. 428 absent, 412 stale, 409 when 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
    (_represent and build_bundle_graph both loop BUNDLE_PARTS, and a link is not a
    part) while their classes are in removal.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: ref is 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.