Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/services/jsonld.py
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,11 @@ def map_dataset_to_dcat(
"keywords": dataset.keywords.split(",") if dataset.keywords else [],
"license": dataset.license,
"version": dataset.version,
# The device and shot the dataset is filed under, as plain names rather
# than links, so that other catalogues can filter on them whatever
# addresses they give devices and shots.
"fuel:device": dataset.device_name,
"fuel:shot": dataset.shot_id,
}

if dataset.persistent_identifier:
Expand Down
24 changes: 23 additions & 1 deletion docs/dcat-jsonld.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,26 @@ The record has no route of its own: its identifier is the resource's with `#reco
resolves to the document that holds it. A catalogue that lists other resources, such as the
catalogue of devices, embeds their nodes without their records.

## Device and shot

A dataset filed under a device, or under one of the device's shots, names them in `fuel:device`
and `fuel:shot`:

```json
{
"@type": "dcat:Dataset",
"@id": "https://example.org/datasets/13",
"title": "Equilibrium (Shot 30421)",
"fuel:device": "mast",
"fuel:shot": "30421"
}
```

The values are the device's `name` and the shot's ID as plain text, not links to the device and
shot documents. Other catalogues give the same device and shot their own addresses, but can use
the same names, so a search across catalogues can filter on these values. A dataset attached to a
device but no shot has `fuel:device` only, and a global dataset has neither.

## Dataset vs Distribution

FDS follows the [W3C DCAT ontology](https://www.w3.org/TR/vocab-dcat/): a **`dcat:Dataset`** is the abstract metadata entity describing *what* the data is, while a **`dcat:Distribution`** is a concrete physical access path describing *how* to retrieve it.
Expand Down Expand Up @@ -393,7 +413,7 @@ See [Provenance](provenance.md) for the model behind these terms.
| `oa` | `http://www.w3.org/ns/oa#` | Web Annotation, used by `dqv:QualityAnnotation` (`oa:motivatedBy`, `oa:hasBody`) |
| `adms` | `http://www.w3.org/ns/adms#` | `adms:identifier`, a persistent identifier registered elsewhere |
| `skos` | `http://www.w3.org/2004/02/skos/core#` | `skos:notation`, the value of that identifier |
| `fuel` | `https://w3id.org/fuel/ns#` | Fusion Energy Lexicon, the role concepts used by `prov:hadRole` and `dcat:hadRole` |
| `fuel` | `https://w3id.org/fuel/ns#` | Fusion Energy Lexicon: the role concepts used by `prov:hadRole` and `dcat:hadRole`, and a dataset's `fuel:device` and `fuel:shot` |

## Ontology mapping summary

Expand All @@ -412,6 +432,8 @@ See [Provenance](provenance.md) for the model behind these terms.
| "dataset produced by" | `prov:wasGeneratedBy` | PROV-O |
| "activity used input / instrument" | `prov:used` + `prov:qualifiedUsage` (`prov:hadRole`) | PROV-O |
| "activity associated with agent" | `prov:wasAssociatedWith` + `prov:qualifiedAssociation` (`prov:hadRole`) | PROV-O |
| Dataset `device_name` | `fuel:device`, as plain text | FuEL |
| Dataset `shot_id` | `fuel:shot`, as plain text | FuEL |
| `persistent_identifier` | `adms:identifier` → `adms:Identifier` | ADMS, DCAT-AP |
| `issued` (publication date) | `dct:issued` on the resource | Dublin Core, DCAT 3 |
| producing Activity's `ended_at` | `prov:generatedAtTime` on the resource | PROV-O |
Expand Down
48 changes: 48 additions & 0 deletions tests/api/v1/test_dataset_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -913,3 +913,51 @@ def test_temporal_coverage_roundtrip(
assert cov["@type"] == "dct:PeriodOfTime"
assert cov["startDate"].startswith("2024-03-15T14:00:00")
assert cov["endDate"].startswith("2024-03-15T14:30:00")


def test_dataset_jsonld_names_its_device_and_shot(
test_client: TestClient, session: Session, admin_user_token: dict
):
DeviceService(session).create(
DeviceCreate(name="MAST", type="Spherical"), user=admin_user
)
ShotService(session).create(
ShotCreate(id="30421", device_name="MAST"), user=admin_user
)
datasets = DatasetService(session)
on_shot = datasets.create(
DatasetCreate(
name="efit",
device_name="MAST",
shot_id="30421",
url="s3://mast/30421/efit",
),
user=admin_user,
)
on_device = datasets.create(
DatasetCreate(name="geometry", device_name="MAST", url="s3://mast/geometry"),
user=admin_user,
)
on_nothing = datasets.create(
DatasetCreate(name="constants", url="s3://global/constants"),
user=admin_user,
)
session.commit()
ld = {**admin_user_token, "Accept": "application/ld+json"}

shot_node = resource(
test_client.get(f"/v1/datasets/id/{on_shot.id}", headers=ld).json()
)
device_node = resource(
test_client.get(f"/v1/datasets/id/{on_device.id}", headers=ld).json()
)
global_node = resource(
test_client.get(f"/v1/datasets/id/{on_nothing.id}", headers=ld).json()
)

assert shot_node["fuel:device"] == "mast"
assert shot_node["fuel:shot"] == "30421"
assert device_node["fuel:device"] == "mast"
assert "fuel:shot" not in device_node
assert "fuel:device" not in global_node
assert "fuel:shot" not in global_node
Loading