Scenario Bundles — architecture & developer guide#
Living document
This page is a developer-oriented map of how the Scenario Bundles feature is wired together. It is expected to grow and be corrected over time — if you touch this area and something below is stale, please update it in the same PR. Line numbers are pointers, not contracts; treat them as "look near here".
The user- and API-facing overview lives on the Scenario Bundles feature page (what a bundle is, and the create/get/update/delete JSON API). This page is about the code: which app owns what, how the layers talk, and where to make a change.
The three layers#
A scenario bundle is authored in a React UI, processed by a Django app
(factsheet), and stored as RDF triples in the OEKG (a Jena Fuseki triple
store), with terms drawn from the OEO ontology.
flowchart LR
subgraph Browser
UI["React frontend<br/>factsheet/frontend/src"]
end
subgraph Django
V["factsheet app<br/>views.py / urls.py / helper.py"]
C["factsheet/oekg/connection.py<br/>(SPARQL + OWL graph setup)"]
Q["oekg app<br/>oekg/sparqlQuery.py"]
end
subgraph Stores
F[("OEKG<br/>Jena Fuseki<br/>triple store")]
O[("OEO ontology<br/>oeo-full.owl")]
end
UI -- "axios JSON<br/>/scenario-bundles/*" --> V
V --> C
Q --> C
V -- "read term lists" --> O
C -- "SPARQL query/update" --> F
Q -- "SPARQL query/update" --> F
- Frontend —
factsheet/frontend/src/(React, built with Vite). Authoring, overview, comparison and history views. - Backend — the Django
factsheetapp (URL prefixscenario-bundles/). Turns JSON payloads into RDF and back. - Graph store (OEKG) — Apache Jena Fuseki, reached over SPARQL.
- Ontology (OEO) — the Open Energy Ontology, the source of the controlled vocabulary (sectors, technologies, study descriptors, …) offered in the form.
Where the code lives#
| Concern | Location |
|---|---|
| Django app (views, URLs, helpers) | factsheet/ |
URL routing (scenario-bundles/ prefix) |
oeplatform/urls.py, factsheet/urls.py |
| React source | factsheet/frontend/src/ |
| Frontend build config | vite.config.mjs (entry factsheet: ./factsheet/frontend/src/index.jsx) |
| OEKG/OEO connection + query wrappers | factsheet/oekg/connection.py, factsheet/oekg/filters.py, factsheet/oekg/namespaces.py |
| Reusable SPARQL query module | oekg/sparqlQuery.py (top-level oekg app) |
| SPARQL UI (YASGUI) | oekg app (oekg:main) |
Two things are called oekg
factsheet/oekg/— an internal package of thefactsheetapp. Holds the connection setup: theSPARQLWrapperclients (sparql,update_endpoint) and the in-memory OEO graph (oeo,oeo_owl).oekg/(repo root) — a separate Django app with the reusablesparqlQuery.pyquery functions, models, views, and the YASGUI SPARQL explorer. See its ownoekg/README.md.
New OEKG-interaction functionality should live in / extend the top-level
oekg app; the factsheet app imports from it.
Frontend#
Django serves a single template for every scenario-bundles route
(factsheets_index_view, factsheet/views.py); the React app reads
window.location.pathname and routes client-side in
factsheet/frontend/src/App.jsx:
| Route | Component | Purpose |
|---|---|---|
scenario-bundles/main |
home.jsx → components/customTable.jsx |
All-bundles overview / listing + filter |
scenario-bundles/id/<uuid> or /new |
components/scenarioBundle.tsx |
Create / edit / view one bundle (tabbed form) |
scenario-bundles/compare/… |
comparisonBoardMain |
Compare bundles |
scenario-bundles/oekg_history, …/oekg_modifications |
history / diff views | Change history |
The bundle authoring form (scenarioBundle.tsx) is organised into tabs; the
"Sectors and technology" tab and the study descriptors section are the
parts fed by the OEO (see below). The frontend talks to Django with axios,
posting/getting JSON to scenario-bundles/<name>/ endpoints; a CSRF token is
included on write requests.
Backend & data flow#
The factsheet views translate between the frontend JSON (see the
example payloads) and RDF triples in the OEKG.
- Bundle lifecycle —
add/,get/,update/,delete/map to view functions infactsheet/views.py. On write, the JSON is parsed and emitted as triples (e.g. sectors/divisions via predicateOEO_00390079); on read, triples are re-assembled into the JSON the form expects. - Populating the form's option lists —
populate_factsheets_elements_view(factsheet/views.py, URL namepopulate-factsheets-elements) returns the controlled-vocabulary lists the form renders:sector_divisions,sectors,scenario_descriptors,technologies. The React form fetches this once when it loads (scenarioBundle.tsx). - Listing & filtering — the overview (
customTable.jsx) fetchesscenario-bundles/all/; filter queries run throughoekg/sparqlQuery.py(e.g.scenario_bundle_filter_oekg,list_factsheets_oekg).
Two ways to reach the OEO#
Both patterns are established in the codebase; pick per use case:
- In-memory OWL graph —
oeo(anrdflib.Graphparsed from the on-diskoeo-full.owl) andoeo_owl(owlready2), set up infactsheet/oekg/connection.py. Used for ontology-structure reads: class hierarchies, labels, definitions (IAO_0000115/ SKOS /rdfs:comment). Seebuild_sector_dropdowns_from_oeoandget_all_sub_classesinfactsheet/helper.py. - Live SPARQL endpoint — the
sparql/update_endpointclients (SPARQLWrapper) infactsheet/oekg/connection.py, driven fromoekg/sparqlQuery.py. Used for querying/updating the OEKG instance data (the bundles themselves). More efficient for data than parsing a graph into Python objects.
The OEO-driven form fields#
Several form fields offer terms from the OEO. All of them are now sourced from
the ontology; the payload is assembled by populate_factsheets_elements_view
and served at scenario-bundles/populate_factsheets_elements/.
| Field | Sourced from | Dynamic? |
|---|---|---|
| Sectors (under a division) | OEO graph via is defined by (OEO_00000504), factsheet/helper.py |
✅ queried live |
| Sector divisions | OEO graph, build_sector_dropdowns_from_oeo, factsheet/helper.py |
✅ queried live |
| Study descriptors | OEO graph, terms annotated oekg annotation (OEO_00020425) with a value starting study descriptor, factsheet/helper.py |
✅ queried live |
| Technologies | OEO graph, served by populate_factsheets_elements_view |
✅ queried live |
Both builders are memoized at module level on first call, because the OEO only changes when the process restarts.
Sector divisions#
build_sector_dropdowns_from_oeo returns (divisions, sectors). Two things
about it are easy to get wrong and are pinned by
factsheet/tests/test_sector_dropdowns.py:
- Divisions are modelled two ways in the OEO. Some carry their members as
individuals (KSG, CRF 2006); others are classes whose members declare the
division through an
rdf:typerestriction (NC/BR, EU legislation). An individual-only query misses the second kind entirely — which is what the former hardcoded list papered over. Each division reports which it is viakind(individualsortree). - A division with no members is still listed (NACE has none), and the
division asserting
is defined byabout itself is filtered out of its own options — CRF 2006 offers 108, not 109.
The last entry is the Other division, which carries the full sector tree
rather than a flat list. The legacy flat sectors list is still served
alongside, so older consumers keep working.
Study descriptors#
Served as study_descriptors, an array of [label, iri, definition] triples —
the same shape as the former hardcoded StudyKeywords array, so consumers did
not have to change their rendering. The annotation match is deliberately a
prefix, because the OEO carries both study descriptor and the inconsistent
study descriptor tag.
Three places consume the list, by two different routes — worth knowing before changing the shape:
- bundle edit checkboxes and overview chips —
scenarioBundle.tsx, which already fetches the populate endpoint for its other fields and readsdata.study_descriptorsstraight off that payload - the all-bundles filter dialog —
FactsheetFilterDialog.jsx - the comparison board —
comparisonBoardItems.jsx
The latter two use the useStudyDescriptors hook
(scenarioBundleUtilityComponents/useStudyDescriptors.js), which holds a
module-level cache and a shared in-flight promise, so N mounts trigger one
request. It also exports getStudyDescriptors() for plain helper functions,
returning [] until the fetch resolves.
customTable.jsx is not a consumer: it holds the selected-keyword state and
passes it down to the filter dialog, which resolves the labels itself.
Related surfaces#
- OEKG SPARQL explorer — the
oekgapp exposes a YASGUI query UI (oekg:main), linked from the Scenario Bundles navbar dropdown (base/templates/base/_header.html). - OEKG chat — an external chatbot for asking questions about the OEKG,
https://oekg-chat.openenergyplatform.org/. (Being linked from the Scenario Bundles nav and the overview page — see the wayfinder map.) - OEKG Web-API — see OEKG API for the
read-only SPARQL endpoint and
Writing scenario bundles for the
REST API that writes them. The singular
scenario-bundle/scenario/manage-datasets/route is superseded by that API and still served; it is described in the reference under Scenario Bundles (legacy).
How to extend this feature#
- Add / change a form option list: decide OWL-graph vs SPARQL (above), add
or adjust the query in
factsheet/helper.py(ontology structure) oroekg/sparqlQuery.py(instance data), expose it throughpopulate_factsheets_elements_view, and consume it inscenarioBundle.tsx. - Add a bundle field: thread it through the create/get/update views in
factsheet/views.py(JSON ⇄ triples) and the matching form tab inscenarioBundle.tsx. - Rebuild the frontend:
npm run build(Vite; output underassets/, served via django-vite). See the frontend workflow. - Add an addressable sub-resource to the REST API: add a field table and a
BundlePartentry, then a route. The machinery works against a subject plus a table rather than against the bundle, so a new part is a table entry and a route — not a module. See The API write path.
The API write path#
The feature has two write paths, and everything above this section describes
the first one: the UI's. Session-authenticated RPC endpoints in factsheet/
build the bundle triple by triple through rdflib, which is a thin client over
Fuseki — so a create writing ~200 triples is ~200 HTTP requests, and it is not
atomic.
The second is the OEKG REST API under /api/v0/scenario-bundles/, in the
oekg app. It is a standalone surface built beside the UI's rather than on top
of it, and it differs from the UI path in four ways a contributor needs to know
before touching it:
- One request, one transaction. The API does not use rdflib as transport. It
builds the graph in memory and sends one SPARQL update request, and one
update request is one transaction across
;-separated operations. A write therefore either lands whole or not at all. - The shape is enforced before the write. The canonical SHACL shape comes
from the
oekgrepository as a build-time artifact —manage.py fetch_oekg_shapes— and the API validates the bundle's post-state in-process before committing. Validating aPATCHdiff instead would pass vacuously, because nothing targets an untyped node. - A write is judged by what it introduces. The pre-state is validated too, and only violations the write adds are refused. Without this the API could not write to any bundle the browser had created. A create has no pre-state and so stays strict.
- Writes are guarded by a version. Every mutating request carries the
version it believes it is editing; the guard binds that version and the
bundle's existence into the update's
WHERE, and reads a write token back to tell a winner from a loser.
Two constraints hold for anything added here. The API must not import
factsheet/oekg/connection.py, which parses the full OEO at module import; and
the graph store and Postgres cannot share a transaction, so the graph commits
first and a failed history write is reported rather than rolled back.
The write sequence#
The sequence every mutating endpoint shares — exists, owned, precondition, validate the whole bundle, guard, read back, record — lives in one place, so an endpoint is a payload and a field table rather than a repetition of that order.
One write to a bundle, whatever part of it the request names.
Every mutating endpoint in this API does the same five things in the same order, and gets them wrong in the same ways if it does them itself:
- the bundle has to exist, or there is nothing to write to;
- the caller has to own it;
- the caller has to say which version it is editing;
- the whole bundle with the change in it has to satisfy the shape -- every constraint in the shape is bundle-local, so a part on its own is not a unit the shape can judge -- but only as far as this write is responsible for it: a violation the bundle already had is not this caller's to answer for, and refusing it would make an inherited defect unfixable through the API;
- the write has to be guarded on that version from inside, and read back,
because the store answers
200whether or not the guard held.
So the sequence lives here once rather than in each view. open_bundle does 1,
require_write does 2 and 3, and apply does 4 and 5 and records the history.
They are three calls rather than one so a sub-resource view can check that
its part exists between the first and the second, and keep the same order
one level down. A view is then only the part that differs: which triples change.
A whole-bundle delete is the one write that does not end in apply, because
there is no bundle left to validate or to version afterwards. It takes the same
first two steps, adds confirm_deletion -- the retyped acronym, which only this
write asks for -- and ends in destroy. Everything it does differently is
written down there.
Refusals travel as exceptions. A helper that returns either a value or a refusal makes every call site test which it got, and one forgotten test is a refusal silently ignored -- so a refusal is raised and a view catches it in the same place it already catches the store's failures.
SPDX-FileCopyrightText: 2026 Jonas Huber https://github.com/jh-RLI © Reiner Lemoine Institut SPDX-License-Identifier: AGPL-3.0-or-later
BundleWrite
dataclass
#
A bundle read, checked, and ready to be changed.
Source code in oekg/writes.py
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 | |
gaps
property
#
What was lost after the graph committed, if anything was.
Empty when everything landed, so a client does not have to check something on every response to learn that the ordinary thing happened; these keys are there to name the exception.
apply(*, removed, added, verb, resource_type=BUNDLE_CLASS, resource_uuid=None, guard='')
#
Validate the post-state, write it under the guard, record it.
guard is an extra pattern bound into the write's own WHERE, for
a condition the bundle's version cannot express -- a delete uses it to
assert that nothing outside the bundle started citing the nodes it is
about to remove. It shares the 409, because the answer to either is
the same: read again and retry.
Raises Refused with a 400 if the shape objects to something this
write introduced, and with a 409 if the guard did not hold. Nothing
is written in either case, and nothing on this object is updated either
-- a caller that catches a refusal must not find a post-state
describing a write that did not happen.
Source code in oekg/writes.py
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 | |
confirm_deletion(request)
#
Refuse unless the caller retyped this bundle's acronym. Returns it.
Beside require_write because it is the same kind of thing -- what a
request has to carry before it may proceed -- and only a whole-bundle
delete asks for it. The acronym is read from the bundle rather than
taken from the caller, which is the whole point.
Raises Refused with a 400.
Source code in oekg/writes.py
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | |
destroy(acronym)
#
Delete this bundle, its bookkeeping and its records of ownership.
Not apply, and every difference is a consequence of there being no
bundle afterwards:
- Nothing to validate. The shape judges a bundle; an absent one is not a worse bundle, it is no bundle.
- No version bump. The version node goes with the bundle it counts. Leaving it is what the browser's delete does (issue #2440), and it leaves a node asserting that a bundle is at version 4 when there is no bundle -- which the next write would then guard against and match.
- The read-back asks whether the bundle is gone, not whether a token landed. A delete's own signal is absence, and absence needs no token to be unambiguous: nothing else in this API can make a bundle stop existing, so finding it still there can only mean the guard did not hold.
- What follows the commit is different too. The ownership rows go, and the history records an event rather than a diff.
Returns what the delete came to -- which nodes went and which were kept because something outside still cites them. The plan is made here rather than by the caller, so the one place that decides what a delete reaches is the one place that writes it.
Raises Refused with a 409 if the guard did not hold -- either the
bundle moved since it was read, or something outside it started citing
a node this plan was about to delete. The advice is the same for both:
read it again and retry.
Source code in oekg/writes.py
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 | |
labels_of(iris)
#
Labels for referenced nodes, answered from the bundle where possible.
Source code in oekg/writes.py
139 140 141 | |
require_write(request)
#
Refuse unless this caller may write, and said which version.
Raises Refused with a 403, a 428 or a 412.
Source code in oekg/writes.py
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | |
open_bundle(request, uid)
#
Read a bundle, or refuse with a 404. Raises Refused.
Existence first, as it is for the two-step delete: one order for the
whole API rather than one per endpoint. Reads are public, so answering 404
before authorisation reveals nothing a GET would not -- and a request for
something that is not there should hear that, rather than being told its
precondition is missing for a resource that does not exist.
Separate from require_write so a sub-resource view can check that its
part exists in between, and keep the same order one level down.
Source code in oekg/writes.py
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 | |
refuse_bundle_renames(payload, known_labels)
#
refuse_renames for a whole-bundle payload, nested parts included.
The pair of bundles.bundle_referenced_iris: that one finds the IRIs a
whole payload points at, this one refuses the ones it would rename. Two
endpoints take a whole bundle -- the create and the replace -- and a loop
written twice is a loop that will cover the parts in one of them and not in
the other the next time a part is added.
Source code in oekg/writes.py
349 350 351 352 353 354 355 356 357 358 359 360 361 | |
refuse_renames(payload, known_labels, fields)
#
Refuse a payload that gives an existing node a different label.
The API offers no rename. Shared IRIs stay shared -- that is the point of a graph -- but a shared contact, organisation, funder or region is cited by other bundles, so letting one payload rewrite its label would change every one of them. Referencing such a node is allowed; renaming it is not, and the difference is the label sent alongside the iri.
Driven by the field table rather than by a list of field names, because a list stops covering a table the moment that table gains a node field, and the refusal then quietly passes writes it should refuse.
Source code in oekg/writes.py
364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 | |
What a mutating request has to carry before it is allowed to proceed.
Two guards live here, and they defend different accidents. The version catches somebody changed this since you looked. The retyped acronym catches right verb, wrong identifier -- a pipeline looping over a list and reaching the wrong entry -- which no version can catch, because the version it names is the correct current version of the wrong bundle.
Only the whole-bundle delete asks for the second one. Ceremony is proportional to blast radius: everything else in this API can be written again.
If-Match is required on every mutating call and there is no opt-out. The
alternative — optional, with same-field writes falling back to last-write-wins
— was on the table with a worked example and declined: a client that does not
know about the header would then overwrite somebody's change and neither of
them would be told. The cost is named rather than discovered: deliberate
overwriting is not reachable, and a repair pipeline that wants it must read
first, every time.
Three refusals, because they mean three different things to a client:
- 428 you did not say which version you were editing;
- 412 you said one, and it is not the current version;
- 409 the server's own guard fired — the bundle moved between the read the validation needed and the write. That one is raised at the write, not here.
SPDX-FileCopyrightText: 2026 Jonas Huber https://github.com/jh-RLI © Reiner Lemoine Institut SPDX-License-Identifier: AGPL-3.0-or-later
confirmation_refusal(request, acronym)
#
Why this delete may not proceed on its confirmation, if it may not.
The acronym is retyped as a query parameter and compared exactly with
the one stored. Exactly, because the whole value of the check is that it
cannot be satisfied by a value the caller already had in hand for another
bundle -- normalising it away would let NEMO-2030 confirm a delete of
nemo 2030, which is the confusion the check exists to catch.
Both refusals are 400 rather than 412: nothing here is a precondition
on the bundle's state. A missing or wrong token is a badly formed request,
and the bundle is exactly as the caller last read it.
Source code in oekg/preconditions.py
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | |
precondition_refusal(request, version)
#
Why this request may not proceed on its precondition, if it may not.
Source code in oekg/preconditions.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | |
versions_named(request)
#
The versions an If-Match names, or None if it names none.
Lenient in what it accepts and strict in what it emits: a weak validator or a bare number is read as the version it plainly is, while a value that is not a version simply matches nothing and is refused as stale.
* names no version. It is a legal header value, and it satisfies the
letter of the precondition while withholding the one thing the precondition
is for, so it reads here as an absent header rather than as a match.
Source code in oekg/preconditions.py
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | |
Who may write a scenario bundle, and what happens to that record.
The rule is asked by every mutating endpoint, because ownership is asked in half a dozen places -- sub-resources, the two-step delete, replace -- and a rule spelled out at each of them would drift at the first change. When the group system is connected to bundles, this is the place that learns about it.
The delete's counterpart lives here too: ownership records are removed with the bundle they describe, which the platform has never done.
Two properties are deliberate rather than incidental:
- Ownership is asked of the access-control model, not compared against a creator. Multi-owner bundles already exist -- an admin command adds a second owner -- so a single-owner comparison would be wrong today, not just after some future change.
- A bundle with no ownership record is administrator-only. Records of unknown provenance fail closed. That is the platform's existing behaviour for these bundles, kept on purpose rather than inherited by accident.
SPDX-FileCopyrightText: 2026 Jonas Huber https://github.com/jh-RLI © Reiner Lemoine Institut SPDX-License-Identifier: AGPL-3.0-or-later
forget_ownership(uid)
#
Remove the records saying who owns uid. Returns whether it worked.
Called after a bundle has been deleted, because ownership data that outlives the thing it describes is the platform's existing behaviour and is not defensible: the rows accumulate, they name a bundle nobody can read, and the next bundle minted at that identifier -- which cannot happen today, but is exactly the kind of thing a later change makes possible -- would inherit an owner nobody granted.
Never raises. The graph has already committed by the time this runs, so a failure here cannot be answered with an error without denying a delete that happened. A leftover row is inert: it grants access to nothing, because every endpoint checks the graph for the bundle first.
Source code in oekg/permissions.py
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | |
may_write_bundle(user, uid)
#
Whether user may change the bundle uid.
Source code in oekg/permissions.py
34 35 36 37 38 39 40 | |
Validation and the shape#
Shape validation for scenario bundles, in one function.
The post-state is what gets validated -- the bundle as it would be after
the write, assembled in memory, before anything is sent to the store. Not the
diff: a diff carries no rdf:type, so no shape targets it and it conforms
vacuously. That would not be weak validation, it would be a confident false
pass.
The whole graph is unnecessary. The shape has no cross-bundle constraint, so one bundle's post-state is a complete unit.
The label subset is merged in for the same reason it exists: ex:CommonShape
requires exactly one rdfs:label on every OEO term a bundle picks, and the
payload carries labels only for the nodes it mints. Only the labels of terms
the post-state actually names are merged -- see _with_the_labels_it_names.
One seam, one function: post-state graph in, violations out. The engine behind it is then swappable -- the fallback if the library ever stalls is Jena's own SHACL engine, which this stack already deploys.
SPDX-FileCopyrightText: 2026 Jonas Huber https://github.com/jh-RLI © Reiner Lemoine Institut SPDX-License-Identifier: AGPL-3.0-or-later
ShapeViolation
dataclass
#
One thing the shape objects to, in the shape's own words.
Source code in oekg/validation.py
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | |
introduced_violations(before, after)
#
What after violates that before did not, and how many it inherited.
A write is judged by what it adds. The alternative -- the post-state must conform, full stop -- sounds stricter and is, but it makes an existing defect unfixable through this API: the fields most often missing are the ones a person would supply by patching, and the patch would be refused for the very thing it came to fix. The promise that matters survives either way, because nothing invalid is written by this API.
Compared as a multiset, so a bundle already missing one required field may not come out missing two. And by violation identity -- message, focus node, path and value together -- so swapping one violation for another counts as introducing one, which comparing counts alone would miss.
Both graphs must be assembled the same way. Hand this the pruned pre-state, not the raw read, or the two differ by how they were built rather than by what the write did, and that shows up as violations nobody introduced.
Source code in oekg/validation.py
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | |
validate_post_state(post_state)
#
Validate a bundle subgraph. An empty list means it conforms.
Source code in oekg/validation.py
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | |
The canonical SHACL shape, read as data.
The shape is the contract for what a scenario bundle is, and it is still
moving in its own repository. So nothing here is copied into Python: the
enumerations a payload may pick from and the messages a rejection carries are
both read from the artifact manage.py fetch_oekg_shapes puts on disk.
That artifact is the cache key. A redeploy with a new pin changes the file, and the file's identity changes with it, so the cache invalidates itself and no process serves yesterday's enumerations.
Two things this module does NOT do:
- It does not fall back. A missing artifact raises rather than validating against nothing, because a validator that silently disappears is worse than one that is absent loudly.
- It does not translate.
message_forreturns the shape's own wording, so the API and the shape can never disagree about the same rule. The validator in use does not honoursh:resultMessageitself, and relying on an engine's leniency for the API's error text would be fragile anyway.
SPDX-FileCopyrightText: 2026 Jonas Huber https://github.com/jh-RLI © Reiner Lemoine Institut SPDX-License-Identifier: AGPL-3.0-or-later
ShapeUnavailable
#
Bases: Exception
The shape artifacts are not on disk, so nothing can be validated.
Source code in oekg/shape.py
37 38 | |
constraint_message(property_iri)
#
The shape's own wording for the property shape constraining property_iri.
Used so a rejected pick is explained in the shape's words rather than in a second set written here, which could then disagree with it.
Source code in oekg/shape.py
76 77 78 79 80 81 82 | |
enumeration(property_iri)
#
The IRIs a payload may pick for property_iri, per the shape's sh:in.
Empty for a property the shape does not constrain by enumeration -- which a caller must treat as "not enumerated", never as "nothing is allowed".
Source code in oekg/shape.py
67 68 69 70 71 72 73 | |
label_graph()
#
The rdfs:label subset the shape's targets need to satisfy it.
Source code in oekg/shape.py
46 47 48 | |
labels_by_term()
#
The same subset as a lookup table: term IRI to its label literals.
The subset is flat -- one predicate, rdfs:label, on IRI subjects -- so
a caller needing the labels of a handful of terms can ask for exactly
those instead of merging all 2,054 triples to reach twenty. Read-only,
because it is the cached artifact and not a copy of it.
The literals are a tuple rather than a single value on purpose: the artifact holds one label per term today, and a validator asking "how many labels does this term have" must see what is there rather than what the extraction promised.
Source code in oekg/shape.py
51 52 53 54 55 56 57 58 59 60 61 62 63 64 | |
message_for(source_shape)
#
The shape's own wording for a violation raised by source_shape.
Source code in oekg/shape.py
85 86 87 88 89 90 | |
shape_graph()
#
The SHACL shape itself.
Source code in oekg/shape.py
41 42 43 | |
Versioning#
A bundle's version: what it is, where it lives, and how a write guards on it.
Two writers on the same field is the failure no amount of shape validation catches -- a perfectly valid change silently destroying one made a second earlier -- and a programmatic API makes it likelier than the user interface does, because scripts retry, run in parallel and do not look at the screen first.
The version is a monotonic counter, not an opaque token, because two other parts of this API need it to be one: the two-step delete uses it as its confirmation, and the history uses it as the natural key of "which change produced this state".
The triple points at the bundle, not away from it. The bundle's shape is
sh:closed with only rdf:type ignored, so <bundle> oekg:version 17
would make every bundle this API writes invalid. sh:closed constrains a
focus node's outgoing properties, so the bundle as an object is untouched,
and nothing in the shape targets the version node: it is untyped, and
oekg:versionOf is not one of the predicates whose objects the shape
validates. The guard therefore works without editing the shape -- and the
version triples stay out of a read for free, because a read walks outward from
the bundle and these point inward.
The version node's IRI is derived from the bundle's, so a compare-and-set can address it in the same request that tests it, with no lookup first.
Why a write token. SPARQL's DELETE/INSERT/WHERE guard applies only when
its pattern matches, but the store answers 200 and changes nothing when it
does not, so the write itself carries no signal. Reading the version back is
not enough either: two writers guarding on version 17 both intend 18, so
finding 18 afterwards does not tell a loser from a winner. Each write therefore
stamps a token only its own writer could have chosen, and reads that back. It
costs no extra round trip -- the token comes back with the version the write
already has to read -- and it is the difference between reporting a conflict
and reporting a success that did not happen.
SPDX-FileCopyrightText: 2026 Jonas Huber https://github.com/jh-RLI © Reiner Lemoine Institut SPDX-License-Identifier: AGPL-3.0-or-later
BundleVersion
dataclass
#
The version a read saw, and the token the write before it left behind.
Both come from one query, because a write needs both: the number to guard on, and the token to clear so the next read-back stays unambiguous.
Source code in oekg/versioning.py
61 62 63 64 65 66 67 68 69 70 71 72 73 74 | |
guarded_operation(store, uid, version, token, delete=None, insert=None, condition='')
#
One update operation that applies delete/insert at version.
The version bump rides in the same operation as the change, so the two
cannot come apart: one request is one transaction, and the guard is the
request's own WHERE.
The guard is the version and the bundle's existence, plus whatever
condition a caller adds. A condition the version cannot express has to
be bound in here, because the version only moves when this bundle is
written: a delete asserting that nothing outside still cites the nodes it
is removing is answering a question about the rest of the graph, and no
amount of versioning this bundle would notice that changing.
The cost of a second condition is that a failed guard no longer has one cause. That is why the refusal says "read it again and retry" rather than naming which half fired -- which is the right advice for either.
Source code in oekg/versioning.py
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | |
mint_write_token()
#
A value no competing writer would choose, so a read-back can tell.
Source code in oekg/versioning.py
106 107 108 | |
read_version(store, uid)
#
The version a bundle is at. UNVERSIONED if it has no version node.
Source code in oekg/versioning.py
93 94 95 96 97 98 99 100 101 102 103 | |
version_guard(uid, version)
#
The pattern that holds exactly while uid is a bundle at version.
Written once because two writes need it and they are not the same write: an
ordinary change bumps the version inside this guard, and a whole-bundle
delete removes the node the guard tests. Spelled out at both, the two would
drift, and a guard that has drifted still answers 200.
The bundle's own existence is asserted alongside the version, and it is not redundant: the version node points AT the bundle, so a delete that removes only the bundle's outgoing triples -- which is exactly what the browser's delete does -- leaves the version node behind. On the version alone the guard would then match a bundle that is gone, and the write would resurrect it as untyped orphan triples.
Source code in oekg/versioning.py
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | |
version_iri(uid)
#
The version node for a bundle -- derived, never looked up.
Source code in oekg/versioning.py
77 78 79 | |
version_node_triples(uid, version)
#
The version node as it currently stands -- what a delete has to remove.
Built from the version a read already holds rather than fetched again: the
node has at most these three triples, and the read that produced
version saw all of them.
Empty for an unversioned bundle, because there is no node yet. Deleting the triples anyway would be harmless and would also say, wrongly, that this API knows of a node it has never written.
Source code in oekg/versioning.py
148 149 150 151 152 153 154 155 156 157 158 159 160 161 | |
version_triples(uid, number, token=None)
#
The version node as triples, at number.
Source code in oekg/versioning.py
82 83 84 85 86 87 88 89 90 | |
write_applied(store, uid, token)
#
Whether the write that stamped token is the one that applied.
The signal the guarded update cannot give. False means the bundle moved
between the read the validation needed and the write -- or vanished
entirely -- so nothing of this request was applied.
Source code in oekg/versioning.py
211 212 213 214 215 216 217 218 219 220 221 | |
Transport to the graph store#
The OEKG REST API's own transport to the graph store.
Deliberately not factsheet/oekg/connection.py. That module exposes the
graph as an rdflib Graph over a SPARQLUpdateStore with
autocommit=True, so every add() is its own committed transaction: a
~200-triple bundle costs ~200 requests and 30 s, and an abort halfway leaves
half a bundle behind. It also parses the full ontology at import -- 1.3 GB
resident and ~36 s, per process -- which no request path should pay for.
So the boundary is drawn one step earlier: keep rdflib for building a graph
in memory, drop it as transport. Callers assemble triples in an
rdflib.Graph (which is also what the validator will read), and this module
ships them as one SPARQL request.
That one request is also one transaction, across ;-separated operations --
established by experiment against Fuseki 5.1.0 on TDB2, and re-proved by this
app's own tests rather than taken on trust. It is why an update-in-place is
expressible at all: a delete and an insert in one request either both apply or
neither does.
Two safety properties are built in rather than left to callers:
- The store's own error bodies never escape. Fuseki answers a bad query with a parser dump that echoes the generated query back. It is logged, never raised.
clear()refuses to touch the default graph. It exists for tests, and a helper that couldDROP DEFAULTis one misconfigured endpoint away from erasing the production knowledge graph.
SPDX-FileCopyrightText: 2026 Jonas Huber https://github.com/jh-RLI © Reiner Lemoine Institut SPDX-License-Identifier: AGPL-3.0-or-later
GraphQueryRejected
#
Bases: GraphStoreError
The store refused a read. Its response body is logged, never carried.
Source code in oekg/graph_store.py
80 81 82 83 | |
GraphStore
dataclass
#
A query client and an update client, and no pretence of being a graph.
graph names the target: None is the default graph, which is what
the platform reads and writes in production. Tests point it at a named
graph of their own instead.
Source code in oekg/graph_store.py
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 | |
ask(query)
#
Run an ASK.
Source code in oekg/graph_store.py
156 157 158 | |
clear()
#
Remove this store's named graph. Refuses on the default graph.
DROP rather than CLEAR: CLEAR empties a graph but leaves it in the store, so a persistent dataset would accumulate one empty graph per test per run.
Source code in oekg/graph_store.py
239 240 241 242 243 244 245 246 247 248 249 250 251 252 | |
construct(query)
#
Run a CONSTRUCT or DESCRIBE and return the triples as a graph.
Source code in oekg/graph_store.py
160 161 162 163 164 165 | |
delete_data(triples)
#
The DELETE DATA operation for triples, scoped to the target graph.
Source code in oekg/graph_store.py
197 198 199 | |
from_settings(*, graph=USE_CONFIGURED_GRAPH)
classmethod
#
Build the store the platform is configured to talk to.
graph defaults to settings.OEKG_GRAPH, which is None -- the
default graph, where the platform's bundles live. A test overrides that
setting to work in a graph of its own without reaching into the views.
Passing graph=None explicitly means the default graph and ignores
the setting.
Source code in oekg/graph_store.py
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | |
guarded_modification(where, delete=None, insert=None)
#
A DELETE/INSERT/WHERE operation, scoped to the target graph.
where is the guard, and it is part of the write rather than a check
in front of it: SPARQL applies the templates only if the pattern
matches, so a compare-and-set cannot be overtaken between the test and
the change. The price is that a guard that does not match is
indistinguishable from one that does -- the store answers 200 and
changes nothing either way -- so the caller has to read back for the
signal. That is not a shortcoming of this method; it is what SPARQL
update offers.
Scoping is by WITH, which makes the target graph the default for
every unqualified pattern in all three clauses at once. Writing
GRAPH blocks instead would put the same decision in three places,
and forgetting one of them writes the default graph -- in production,
the graph the platform serves.
Source code in oekg/graph_store.py
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | |
insert(triples)
#
Write every triple in triples in one request.
Source code in oekg/graph_store.py
185 186 187 | |
insert_data(triples)
#
The INSERT DATA operation for triples, scoped to the target graph.
Returned rather than sent, so several operations can be combined into the single request that makes them atomic.
Source code in oekg/graph_store.py
189 190 191 192 193 194 195 | |
is_available()
#
Whether this store can actually be read from and written to.
Probed with a harmless no-op update rather than a read, because the two are not the same permission: Fuseki serves queries to anyone and answers updates with 401 unless credentials are valid. A read-only probe would report a store as usable and leave the write tests failing instead of skipping.
Never raises -- the caller is deciding whether to skip.
Source code in oekg/graph_store.py
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 | |
select(query)
#
Run a SELECT and return its bindings as plain dicts of strings.
Source code in oekg/graph_store.py
148 149 150 151 152 153 154 | |
update(*operations)
#
Send SPARQL update operations as one request, so one transaction.
The operations are the caller's own SPARQL and are sent as written --
this method does not scope them to the target graph. Use
insert_data, delete_data and guarded_modification to build
scoped operations; reach for a hand-written one only where no builder
exists yet, and scope it yourself. An unscoped INSERT DATA writes
the default graph whatever this store targets.
Source code in oekg/graph_store.py
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | |
GraphStoreError
#
Bases: Exception
Base class for every way talking to the graph store can fail.
action names what was refused and detail what the caller may assume
about the graph's state afterwards. Both live on the class so the message
is built from the failure itself rather than from an argument that has to
agree with it.
Source code in oekg/graph_store.py
63 64 65 66 67 68 69 70 71 72 73 | |
GraphStoreUnavailable
#
Bases: GraphStoreError
The store could not be reached at all -- DNS, refused, or timed out.
Source code in oekg/graph_store.py
76 77 | |
GraphUpdateRejected
#
Bases: GraphStoreError
The store refused a write. Its response body is logged, never carried.
Source code in oekg/graph_store.py
86 87 88 89 90 | |
NothingToModifyError
#
Bases: GraphStoreError
A guarded modification was asked for with no triples to change.
Source code in oekg/graph_store.py
97 98 | |
UnsafeClearError
#
Bases: GraphStoreError
Refused to empty the default graph.
Source code in oekg/graph_store.py
93 94 | |
Payloads and triples#
One field table per resource drives both directions, so a field is declared once rather than written twice.
A scenario bundle and its parts, as triples and as payloads.
The field tables live here; the machinery that drives them lives in
oekg.fields, because it is the same problem at every level and there are now
three tables playing it: the bundle, the scenario factsheet and the study
report. Each entry names the property the shape validates, so every field is
traceable to the shape.
What is a part and what is a field is not a judgement call: the shape decides it. A thing carrying its own has-uuid is addressable and therefore a part with its own URL; a set of IRIs with labels stays a field on its parent. Scenario factsheets and study reports carry one, frameworks and models do not.
Two decisions worth stating here rather than leaving to be inferred:
- The server mints identifiers. No client-supplied identifier is accepted, so a pipeline cannot collide with, or overwrite, somebody else's bundle by choosing a value.
- Frameworks and models are minted per bundle. They are fields, not addressable resources, so nothing outside this bundle should reference them. The user interface mints them globally, which is why deleting one bundle today strips labels from every other bundle citing the same model.
SPDX-FileCopyrightText: 2026 Jonas Huber https://github.com/jh-RLI © Reiner Lemoine Institut SPDX-License-Identifier: AGPL-3.0-or-later
BundlePart
dataclass
#
An addressable part of a bundle: what it is, and what it is made of.
One of these per resource the shape gives its own has-uuid. Everything that differs between a scenario factsheet and a study report is in here, which is what lets one implementation of the endpoints serve both -- and what makes adding the next such resource a table rather than a module.
Source code in oekg/bundles.py
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | |
build_bundle_graph(uid, payload, known_labels=None, address=None)
#
A whole bundle: its fields, its parts, and its scenarios' dataset links.
Nesting is accepted here and on the replace endpoint, and nowhere else on
the write path: a bundle POST builds its scenarios and study reports with
it, while a bundle PATCH cannot reach one. That asymmetry is what lets a
pipeline create a whole bundle in one call without giving any call the
power to drop its parts by omission.
address says where one nested dataset link points, given its payload.
It is a callable rather than a URL because half of the answer comes from
the router and the other half from the request the link arrived on -- and
neither belongs in a module about triples.
Source code in oekg/bundles.py
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 | |
build_nested_link_graphs(part, node, payload, address=None)
#
The dataset links nested inside one part's payload, if it takes any.
Only a scenario does. Asked of the part rather than of the key, so a part that never carries links needs no branch here and a payload that names them on a study report is refused by that serializer rather than ignored here.
Source code in oekg/bundles.py
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 | |
build_part_graph(part, bundle, pid, payload, known_labels=None, node=None)
#
One sub-resource, linked to its bundle and carrying its identity.
The uuid goes in twice on purpose: once as the literal the shape requires and the URL names, and once inside the minted IRI. The literal is the identity -- a part the user interface wrote has an IRI this API did not choose, and a lookup by literal finds it anyway.
node names an existing part this is rewriting, for the one caller that
has one: a replace matches a part by the identifier its _meta carries,
and that part may well live at an IRI this API did not mint.
Source code in oekg/bundles.py
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 | |
bundle_delta(uid, payload, pre_state, known_labels=None)
#
resource_delta for a bundle.
Source code in oekg/bundles.py
328 329 330 331 332 333 334 | |
bundle_iri(uid)
#
The IRI a bundle lives at -- the same one the user interface reads.
Source code in oekg/bundles.py
181 182 183 | |
bundle_payload(graph, uid)
#
A bundle's own fields. Sub-resources are not in here.
They have their own URLs, so a bundle read names them rather than nesting
them -- the same asymmetry as the write side, from the other direction. The
view adds that naming; keeping it out of this function is what lets a read
be sent straight back to POST, where scenarios and study_reports mean
create these.
Source code in oekg/bundles.py
365 366 367 368 369 370 371 372 373 374 | |
bundle_referenced_iris(payload)
#
Existing node IRIs a whole-bundle payload points at, nesting included.
One field table knows one level, because only the caller knows which key holds what -- so the walk over the parts is here, where the parts are.
Source code in oekg/bundles.py
315 316 317 318 319 320 321 322 323 324 325 | |
bundle_subgraph(graph, uid)
#
graph narrowed to the bundle, as deep as a read goes.
Applied to a patch's post-state, this is what makes "validate the post-state" mean the state that will actually read back: a node a patch unlinked is no longer part of the bundle, so it is no longer part of what the shape is asked about.
It walks to the same depth the read query does, because the two have to agree: the post-state validated must be the state a subsequent read returns, and a pruner that stopped shorter would hide a scenario's regions from the validator.
Source code in oekg/bundles.py
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 | |
bundle_uid(iri)
#
The identifier inside a bundle's IRI -- the inverse of bundle_iri.
None for an IRI outside the namespace bundles live in, so a listing
reports what it found rather than a segment chopped off the end of an
address it does not recognise. The IRI itself is reported either way.
A uid this API did not mint can come back here: the user interface takes
one from its client and concatenates it. Such a bundle is listed with the
uid it has, and its own URL answers 404 -- which is the read side
reporting an identity problem that predates it, not making one.
Source code in oekg/bundles.py
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | |
find_part(graph, uid, part, pid)
#
The part of this bundle with identifier pid, if it has one.
By the has-uuid literal rather than by rebuilding the IRI, so a part written before this API existed is addressable too.
Source code in oekg/bundles.py
231 232 233 234 235 236 237 238 239 240 | |
part_iri(part, pid)
#
The IRI a sub-resource this API minted lives at.
Derived, but not the identity: the identity is the has-uuid literal, so a part the user interface wrote -- whose IRI this API did not choose -- is still reachable by the same identifier its URL carries.
Source code in oekg/bundles.py
206 207 208 209 210 211 212 213 | |
part_nodes(graph, uid, part)
#
Every part of this kind hanging off this bundle, in the graph given.
Source code in oekg/bundles.py
216 217 218 219 220 221 222 | |
part_payload(graph, node, part)
#
One sub-resource's fields.
Source code in oekg/bundles.py
377 378 379 | |
part_uid(graph, node)
#
The identifier a part node carries, as the shape requires it to.
Source code in oekg/bundles.py
225 226 227 228 | |
One field table per resource, driving triples in both directions.
A resource in this API -- a bundle, a scenario factsheet, a study report -- is a closed set of fields, some literal, some picked from one of the shape's own lists, some minted nodes. That is the same problem at every level, so the machinery is written against a subject and a table rather than against any one resource: give it the subject to hang triples off and the table saying what the fields are, and it can build a create, a patch's delta, and the read that sends either back.
Writing it once is what makes a read return exactly what a write accepts. Neither direction can drift from the other, because there is only one place that says what a field is.
Each table entry names the property the shape validates, so every field is traceable to the shape rather than to a convention somebody remembered.
The server mints identifiers, everywhere and without exception, so a
pipeline cannot collide with -- or overwrite -- somebody else's resource by
choosing a value. And minted nodes are typed: the shape's sh:class
constraints require it, the user interface does not do it, and the live graph
carries the violations that produced.
SPDX-FileCopyrightText: 2026 Jonas Huber https://github.com/jh-RLI © Reiner Lemoine Institut SPDX-License-Identifier: AGPL-3.0-or-later
field_named(fields, name)
#
The one entry in fields called name.
Source code in oekg/fields.py
90 91 92 | |
field_triples(subject, field, value, known_labels=None)
#
The triples one field's value means, and nothing else.
A create needs every field's triples at once; a patch needs exactly the named field's, so that a field nobody mentioned is genuinely untouched rather than deleted and rewritten identically. Both ask here, so the two directions cannot come to disagree about what a field is.
Source code in oekg/fields.py
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | |
linked_field_triples(graph, subject, field)
#
The triples that currently attach field's values to subject.
What a patch of that field removes -- the links only. A referenced contact, organisation, funder or author is shared with other bundles, and a framework or model is on the same "unlink, never delete" footing: nothing here reaches into a node's own triples, so no patch can strip a label that another bundle is displaying.
The cost of that rule is an unlinked node left in the graph. It is unreachable from the bundle, so no read returns it and no shape is asked about it, and deciding which nodes may actually be removed is the typed containment walk's job -- a whole slice of its own, because the allowlist was already wrong once while it was being drafted. Unlinking fails safe; guessing does not.
Frameworks and models share the has-part predicate, so the type is what tells them apart. Deleting by predicate alone would take both.
Source code in oekg/fields.py
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | |
mint_identifier()
#
A new identifier for anything this API creates.
One function rather than one per resource: no client supplies an identifier anywhere in this API, and that rule is easier to keep true where there is a single place it is expressed.
Source code in oekg/fields.py
273 274 275 276 277 278 279 280 | |
minted(field)
#
A new IRI for a node this API is creating, under the field's segment.
Source code in oekg/fields.py
283 284 285 | |
referenced_node_iris(payload, fields)
#
Every existing node IRI this payload points at, for this table.
One table, one level. Nesting is the caller's business, because only the caller knows which key holds what -- a table that reached for a key by name would keep looking for it after being handed a table that has no such key.
Source code in oekg/fields.py
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | |
resource_delta(subject, fields, payload, pre_state, known_labels=None)
#
What a patch of payload removes and what it adds. Nothing else.
Per named field, so a field the payload does not mention contributes to neither side and is genuinely untouched -- not deleted and rewritten identically, which would churn every minted node in the bundle.
A set-valued field that is named is replaced whole: its links go and the payload's take their place. Emptying such a field is therefore a real change, which the shape then judges -- an empty list on a field the shape requires is a rejection, never a silent wipe.
Source code in oekg/fields.py
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | |
resource_payload(graph, subject, fields)
#
Read one resource's triples back into exactly what a write would accept.
Source code in oekg/fields.py
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 | |
resource_triples(subject, node_class, fields, payload, known_labels=None)
#
Assemble the triples one resource's payload means.
The result is the post-state to validate and, unchanged, the thing to write: nothing is added between validating and writing.
known_labels maps an already-existing node IRI to the label it already
carries. Shared IRIs stay shared -- that is the point of a graph -- so a
referenced node keeps its own label and this never writes a second one onto
it. Without that, an additive write would leave a contact other bundles
cite carrying two labels, violating sh:maxCount 1 for all of them.
Source code in oekg/fields.py
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | |
Reading a bundle out of the graph.
Bounded depth, not a transitive closure. A bundle reaches its scenarios in one hop, a scenario's study regions in two, and a region's reference in three -- which is the longest chain the shape allows, so three hops see a whole bundle and fewer do not. A closure would also be correct today and would stop needing thought; it would also be an unbounded walk on a public endpoint, reachable by anyone who can put a triple in the graph.
The version node is unreachable from here, because it points at the bundle rather than away from it. That is what keeps bookkeeping out of every payload without a single line of filtering.
SPDX-FileCopyrightText: 2026 Jonas Huber https://github.com/jh-RLI © Reiner Lemoine Institut SPDX-License-Identifier: AGPL-3.0-or-later
labels_of(store, iris, known=None)
#
The label each of these nodes already carries, for the ones that exist.
A write consults this so a referenced node keeps its own label and never gains a second one -- which would violate the shape for every bundle citing it, not just the one being written.
known is a graph already in hand; anything answered from it costs no
query, so a request that has read its bundle asks the store only about
nodes outside it.
Source code in oekg/reads.py
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | |
read_bundle(store, uid)
#
A bundle and everything reachable from it, or None if there is none.
Source code in oekg/reads.py
26 27 28 29 30 31 | |
History#
What a write leaves behind, and how it reads back.
Two rules, and the second is the one that is easy to get wrong.
Every write records. Not "every write that destroys information" -- the judgement call is what makes an audit trail patchy, and "no entry" would then be ambiguous between created and never touched. So the rule is flat: the history is the log of writes.
The graph commits first, and a failed history write does not fail the
request. The two stores cannot share a transaction. The data is the truth and
the history is the note about it, so answering 500 for a write that succeeded
would provoke exactly the retry that creates duplicates. The price is a
possible gap in the audit trail, and it is named rather than hidden -- in
the response, and in one structured log line -- because a phantom entry would
be worse: it looks like truth.
What is stored is the changed triples, losslessly and untranslated. No field names, no serializer vocabulary, nothing that a later shape change could re-interpret. Field names are computed here at read time instead, from the same field table that builds the triples, so a rendering can never drift from what was stored -- it is derived from it on every read.
SPDX-FileCopyrightText: 2026 Jonas Huber https://github.com/jh-RLI © Reiner Lemoine Institut SPDX-License-Identifier: AGPL-3.0-or-later
changed_fields(uid, removed, added, resource_type=None, resource_uuid=None)
#
The diff, in the vocabulary a client writes in.
Computed on every read rather than stored, so it cannot drift from the triples it describes and a shape change never re-interprets an old row.
resource_type and resource_uuid are the class and identifier the
write was about, both of which the entry already records. Together they say
which vocabulary to render in and which subject in the diff is the
thing being described: a change to a study report is named in the study
report's field names, not in the bundle's, and the two tables share label
while agreeing about almost nothing else. An entry naming no class is read
as a bundle write, which is what every row written before sub-resources
existed is.
A triple this cannot attribute to a field -- the type and label a minted
contact brings with it, say -- is reported with a None field rather
than dropped. Dropping it would make the summary look complete when it is
not, which is the one thing a history must not do.
Every change names its predicate, attributed or not. Without it an unattributed entry would be a list of bare values with nothing saying what they were values of, which is only marginally better than dropping them.
Source code in oekg/history.py
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 | |
record_bundle_deletion(*, bundle_uid, acronym, actor, version_before)
#
Record that a bundle was deleted, and prune what it used to hold.
One event-only line, and no payload. Every other write stores the triples it changed, because a reader needs to know what the change was. A whole-bundle delete stores none, because the triples it removed are the whole bundle: a diff would make the ledger a copy of the thing that was deleted, and deleting would not delete. So the line records that it happened -- who, when, which identifier, which acronym, from which version -- and nothing about what was in it.
And the same reasoning reaches backwards. The bundle's earlier entries keep their structured columns, so the record of how it changed survives, but their payloads go: a history that kept them would let anyone rebuild a deleted bundle from the account of its own deletion. Legacy rows are pruned too, and they are the ones that matter most -- a browser write stored the bundle's whole state, not a diff.
Pruning and the event line are one transaction, because a pruned bundle with no line saying why reads as tampering, and a line with the payloads still under it has not deleted anything.
Never raises, for the same reason record_write does not: the graph
write has already committed, and there is nothing left to undo.
Source code in oekg/history.py
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | |
record_write(*, bundle_uid, verb, actor, version_before, version_after, removed=None, added=None, resource_type=BUNDLE_CLASS, resource_uuid=None)
#
Record one write. Returns whether it was recorded.
Never raises, and that is load-bearing rather than polite: the caller's graph write has already committed, so there is nothing left to undo and nothing useful to tell the client beyond the fact that this note was lost. Anything raised from here would become a 500 for a write that succeeded -- the one outcome the whole ordering exists to avoid -- so the net is cast around every failure and not only around the database's.
Source code in oekg/history.py
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | |
Shared endpoint behaviour#
What every endpoint of the OEKG API shares: its ceilings and its refusals.
Extracted when the second view module appeared and had to import five names
from the first. That import is the signal: helpers every endpoint needs are not
the bundle endpoint's property, and five more view modules are coming --
scenarios, study reports, dataset links, delete, replace. Left where they were,
api_views would become a utility module by accident, and would change
whenever any endpoint's conventions changed.
What belongs here: throttling, the refusals more than one endpoint gives, the existence check they all make first, and the reading of the one query parameter more than one endpoint answers. What does not: anything specific to one resource, which stays with that resource's view.
SPDX-FileCopyrightText: 2026 Jonas Huber https://github.com/jh-RLI © Reiner Lemoine Institut SPDX-License-Identifier: AGPL-3.0-or-later
OekgAPIView
#
Bases: APIView
The base every OEKG endpoint shares: one ceiling, one way to refuse.
Handling Refused here rather than in each view means a new endpoint
cannot forget to, and a refusal decided three calls deep still reaches the
client as the response it was written as.
Source code in oekg/api_support.py
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | |
initial(request, *args, **kwargs)
#
Settle the request's own problems before the handler runs.
?expand= is read here, not in each handler, and that is not
tidiness: a handler that reads it after writing would create the
resource, record the history, bump the version and then answer 400
-- a refusal that refused nothing. Reading it before dispatch makes
"nothing was written" true of every 400 this API gives, and makes it
true structurally rather than by each endpoint remembering.
It is checked ahead of the resource's existence, unlike a payload's problems: an expansion nobody offers is wrong whether or not the bundle is there, and the store should not be read for a response that cannot be given.
Source code in oekg/api_support.py
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | |
Refused
#
Bases: Exception
A refusal, raised where it is decided and returned as it stands.
It carries a whole Response rather than a detail, and OekgAPIView
hands that back untouched. Both halves of that are deliberate:
- Raised, not returned. A helper that returned either a value or a refusal would make every call site test which it got, and one forgotten test is a refusal silently ignored.
- A Response, not an
APIException. The obvious alternative is to subclassAPIExceptionand let the framework render it -- but the framework rewrites every scalar in an error body throughErrorDetail, soNonecomes out as the string"None"and a count as a string. This API's refusals carry structured data with nullable fields, so that would corrupt them. Measured, not assumed.
Source code in oekg/api_support.py
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | |
ScenarioBundleThrottle
#
Bases: AnonRateThrottle
Reads are public, so the public endpoints need a ceiling of their own.
Source code in oekg/api_support.py
55 56 57 58 | |
bundle_exists(uid)
#
Whether there is a bundle at uid. Asked of the graph, not of a table.
Raises GraphStoreError rather than answering False when the store
cannot be reached: "there is no such bundle" and "I could not find out" are
different answers, and a caller has to be able to say 503 instead of 404.
An ASK rather than the read a GET does -- this asks whether the bundle is there, and pulling its whole subgraph across to answer that would make every caller pay for data it throws away.
Source code in oekg/api_support.py
168 169 170 171 172 173 174 175 176 177 178 179 180 181 | |
bundle_in(store, uid)
#
The same question, asked of a store the caller already has.
Separate from bundle_exists because a delete has to ask it of its own
store, inside its own request, and because the two must not drift: a write
reads this back to find out whether its delete applied, and a query that
had drifted would report a success that did not happen.
Source code in oekg/api_support.py
184 185 186 187 188 189 190 191 192 | |
expansions(request, allowed)
#
The expansions this request asked for, refusing one nobody offers.
?expand= is the one query parameter more than one endpoint reads, so the
rule for an unrecognised value lives here rather than being written again
per endpoint with a slightly different wording. It is the same rule an
unknown key gets on a write: refused, not ignored -- a client that
misspells labels should be told, not handed the unresolved
representation and left to work out why the labels are missing.
Comma-separated, so asking for two is asking once. Raises Refused.
Source code in oekg/api_support.py
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | |
is_minted_identifier(uid)
#
Whether uid could have come from this API.
Checked before a value reaches a query: an IRI-unsafe character makes rdflib refuse to build the IRI, which would surface as a 500 rather than the 404 it actually is. That refusal is also what keeps the query free of injection.
Source code in oekg/api_support.py
153 154 155 156 157 158 159 160 161 162 163 164 165 | |
is_safe(request)
#
Whether this request only reads.
Asked by every endpoint that is public to read and closed to write, and
asked here so all of them ask the same question. The safe methods are
named and everything else is closed, rather than the other way round: a
verb a later slice adds is then authenticated by default instead of public
until somebody remembers. The price is that an unsupported verb answers
401 before it can answer 405, which is the cheaper of the two mistakes.
Source code in oekg/api_support.py
103 104 105 106 107 108 109 110 111 112 113 | |
API reference#
For request/response shapes and endpoint URLs, see the Scenario Bundles feature page.
SPDX-FileCopyrightText: 2025 Adel Memariani https://github.com/adelmemariani © Otto-von-Guericke-Universität Magdeburg SPDX-FileCopyrightText: 2025 Adel Memariani https://github.com/adelmemariani © Otto-von-Guericke-Universität Magdeburg SPDX-FileCopyrightText: 2025 Adel Memariani https://github.com/adelmemariani © Otto-von-Guericke-Universität Magdeburg SPDX-FileCopyrightText: 2025 Adel Memariani https://github.com/adelmemariani © Otto-von-Guericke-Universität Magdeburg SPDX-FileCopyrightText: 2025 Adel Memariani https://github.com/adelmemariani © Otto-von-Guericke-Universität Magdeburg SPDX-FileCopyrightText: 2025 Bryan Lancien https://github.com/bmlancien © Reiner Lemoine Institut SPDX-FileCopyrightText: 2025 Christian Winger https://github.com/wingechr © Öko-Institut e.V. SPDX-FileCopyrightText: 2025 Jonas Huber https://github.com/jh-RLI © Reiner Lemoine Institut SPDX-FileCopyrightText: 2025 Jonas Huber https://github.com/jh-RLI © Reiner Lemoine Institut SPDX-FileCopyrightText: 2025 Jonas Huber https://github.com/jh-RLI © Reiner Lemoine Institut
SPDX-License-Identifier: AGPL-3.0-or-later
add_entities_view(request, *args, **kwargs)
#
Add entities to OEKG. The minimum requirements for adding an entity are the type and label.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
HttpRequest
|
The incoming HTTP GET request. |
required |
entity_type
|
str
|
The type(OEO class) of the entity. |
required |
entity_label
|
str
|
The label of the entity. |
required |
entity_iri
|
str
|
The IRI of the entity. |
required |
Source code in factsheet/views.py
1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 | |
create_factsheet_view(request, *args, **kwargs)
#
Creates a scenario bundle based on user's data. Currently, the minimum requirement to create a bundle is the "acronym". The "acronym" must be unique. If the provided acronym already exists in the OEKG, then the function returns a "Duplicate error".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
HttpRequest
|
The incoming HTTP GET request. |
required |
uid
|
str
|
The unique ID for the bundle. |
required |
acronym
|
str
|
The acronym for the bundle. |
required |
abstract
|
str
|
The abstract for the bundle. |
required |
institution
|
list of objects
|
The institutions for the bundle. |
required |
funding_source
|
list of objects
|
The funding sources for the bundle. |
required |
contact_person
|
list of objects
|
The contact persons for the bundle. |
required |
sector_divisions
|
list of objects
|
The sector divisions for the bundle. |
required |
sectors
|
list of objects
|
The sectors for the bundle. |
required |
technologies
|
list of objects
|
The technologies for the bundle. |
required |
study_keywords
|
list of strings
|
The study keywords for the bundle. |
required |
scenarios
|
list of objects
|
The scenarios for the bundle. |
required |
models
|
list of strings
|
The models for the bundle. |
required |
frameworks
|
list of strings
|
The frameworks for the bundle. |
required |
publications
|
list[object]
|
A list of n publications related to the bundle study_name (str): The study name for the bundle. date_of_publication (str): The date of publication for the bundle. report_title (str): The report title for the bundle. report_doi (str): The report_doi for the bundle. place_of_publication (str): The place of publication for the bundle. link_to_study_report (str): The link to study for the bundle. authors (list of objects): The authors for the bundle. |
required |
Returns:
| Type | Description |
|---|---|
|
"Factsheet saved" if successful, "Duplicate error" if the bundle's |
|
|
acronym exists. |
Source code in factsheet/views.py
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 | |
dataset_entry(node)
#
THis function is part of the comparison backend. It helps to retieve data table ID's from the OEKG input / ouput data relations.
This function especially helps to retrieve the table name from a vaity of enties, some of them are referancing external sources which are not directly usable in our comparison backend.
:param node: A predicate from the OEKG related to a Node in the graph which is related to an input or output dataset.
:return: A dict with the dataset label, url and if possible the table name.
Source code in factsheet/views.py
1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 | |
delete_factsheet_by_id_view(request, *args, **kwargs)
#
Removes a scenario bundle based on the provided ID.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
HttpRequest
|
The incoming HTTP GET request. |
required |
id
|
str
|
The unique ID for the bundle. |
required |
Source code in factsheet/views.py
1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 | |
get_entities_by_type_view(request, *args, **kwargs)
#
Returns all entities (from OEKG) with a certain type. The type should be supplied by the user.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
HttpRequest
|
The incoming HTTP GET request. |
required |
entity_type
|
str
|
The type(OEO class) of the entity. |
required |
Source code in factsheet/views.py
1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 | |
populate_factsheets_elements_view(request, *args, **kwargs)
#
This function populates the elements required for creating or updating a factsheet. For example: Elements returned form this function populate dropdown elements which help the user to select sectors, technologies, scenario descriptors etc.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
HttpRequest
|
The incoming HTTP GET request. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
JsonResponse |
A JSON response containing the elements for factsheet creation or update. |
Source code in factsheet/views.py
1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 | |
update_an_entity_view(request, *args, **kwargs)
#
Updates an entity in OEKG. The minimum requirements for updating an entity are the type, the old label, and the new label.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
HttpRequest
|
The incoming HTTP GET request. |
required |
entity_type
|
str
|
The type(OEO class) of the entity. |
required |
entity_label
|
str
|
The label of the entity. |
required |
new_entity_label
|
str
|
The new label of the entity. |
required |
entity_id
|
str
|
The IRI of the entity. |
required |
Source code in factsheet/views.py
1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 | |
update_factsheet_view(request, *args, **kwargs)
#
Updates a scenario bundle based on user's data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
HttpRequest
|
The incoming HTTP GET request. |
required |
uid
|
str
|
The unique ID for the bundle. |
required |
acronym
|
str
|
The acronym for the bundle. |
required |
abstract
|
str
|
The abstract for the bundle. |
required |
institution
|
list of objects
|
The institutions for the bundle. |
required |
funding_source
|
list of objects
|
The funding sources for the bundle. |
required |
contact_person
|
list of objects
|
The contact persons for the bundle. |
required |
sector_divisions
|
list of objects
|
The sector divisions for the bundle. |
required |
sectors
|
list of objects
|
The sectors for the bundle. |
required |
technologies
|
list of objects
|
The technologies for the bundle. |
required |
study_keywords
|
list of strings
|
The study keywords for the bundle. |
required |
scenarios
|
list of objects
|
The scenarios for the bundle. |
required |
models
|
list of strings
|
The models for the bundle. |
required |
frameworks
|
list of strings
|
The frameworks for the bundle. |
required |
publications
|
list[object]
|
A list of n publications related to the bundle study_name (str): The study name for the bundle. date_of_publication (str): The date of publication for the bundle. report_title (str): The report title for the bundle. report_doi (str): The report_doi for the bundle. place_of_publication (str): The place of publication for the bundle. link_to_study_report (str): The link to study for the bundle. authors (list of objects): The authors for the bundle. |
required |
Source code in factsheet/views.py
570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 | |