Research Data Infrastructure for the WeDoWind community — publish wind-related dataset metadata from Zenodo and CKAN to the Open Energy Platform (OEP) using OEMetadata and OMI.
- wedowind
uv syncOptional pre-commit hooks:
uv sync --group dev
uv run pre-commit installCopy .env.dist to .env and set OEP_API_TOKEN (and optionally ZENODO_ACCESS_TOKEN for higher Zenodo API limits).
uv run python -m mappers.zenodo.publish_sources --dry-run
uv run python -m mappers.zenodo.publish_sourcesuv run python -m mappers.zenodo.publish_record --dataset-id "<zenodo-id>" --dry-runuv run python -m mappers.ckan.publish_sources --dry-run
uv run python -m mappers.ckan.publish_sourcesuv run python -m mappers.ckan.publish_record \
--ckan-url "https://energydata.info/en" \
--dataset-id "<package-name>" \
--dry-runFor each file in a Zenodo record or CKAN resource:
- Infer schema — stream the first 2 lines from the source download URL (not stored on disk); OMI/Frictionless infers
schema.fields. - Create empty OEP table —
PUT /api/v0/tables/{name}/with column definitions only (no data rows uploaded to OEP). - Push metadata —
POST /api/v0/tables/{name}/meta/with full OEMetadata via OMI.
sequenceDiagram
autonumber
actor User as User / CLI
participant Src as Source API (Zenodo or CKAN)
participant Map as Mapper (ZenodoToOepMapper or CKANToOepMapper)
participant OMI as OMI
participant OEP as Open Energy Platform (OEP)
User->>Map: Run publish_sources / publish_record
Map->>Src: Pull dataset metadata + file/resource URLs
Src-->>Map: Record/package metadata + file list
loop For each file/resource URL
Map->>Src: Stream first N lines (schema sample)
Src-->>Map: Header + sample rows
Map->>OMI: Infer schema.fields (OMI inspect, fallback frictionless)
OMI-->>Map: Inferred fields + dialect
end
Map->>Map: Build OEMetadata document (dataset + resources)
Map->>OMI: validate_metadata(...)
OMI-->>Map: Validation result (ok/error)
opt Table provisioning enabled
Map->>OEP: PUT /api/v0/tables/{table}/ (empty schema only)
OEP-->>Map: 201/200 created or exists
end
Map->>OMI: update_oep_tables_from_dataset_metadata(...)
OMI->>OEP: POST /api/v0/tables/{table}/meta/
OEP-->>OMI: Metadata update response
OMI-->>Map: Per-table publish result
Map-->>User: Summary (published / failed)
Internal wedowind layers from CLI through OEP publish:
flowchart TB
subgraph cli [CLI entry]
PublishCLI["publish_record / publish_sources"]
end
subgraph map [Mapping layer]
ZenodoMapper["ZenodoToOepMapper"]
CkanMapper["CKANToOepMapper"]
Builder["OemetadataBuilder"]
Resource["OemetadataResource"]
SchemaInf["SchemaInference"]
end
subgraph validate [Validation]
ValidateFn["validate_oemetadata"]
end
subgraph publish [OEP publish layer]
PublishFn["publish_to_oep"]
OepTableEnsure["OepTable.ensure_for_metadata"]
PushFn["push_oemetadata"]
end
PublishCLI --> ZenodoMapper
PublishCLI --> CkanMapper
ZenodoMapper --> SchemaInf
CkanMapper --> SchemaInf
SchemaInf -->|"schema + dialect"| Builder
ZenodoMapper --> Builder
CkanMapper --> Builder
Builder --> Resource
Resource -->|"to_dict() OEMetadata doc"| ValidateFn
ValidateFn --> PublishFn
PublishFn --> OepTableEnsure
OepTableEnsure --> PushFn
For how typed OEMetadata objects are assembled before validation, see OEMetadata composition (Phase 0) below.
OemetadataBuilder and dataclasses in oemetadata.py; entry points ZenodoToOepMapper.map_to_oemetadata and CKANToOepMapper.map_to_oemetadata.
One OEMetadata dataset corresponds to one upstream dataset - record in Zenodo, package in CKAN.
Each OEMetadata resource (one OEP table) corresponds to one attachable file (Zenodo) /resource (CKAN).
| Source | One OEMetadata dataset | One OEMetadata resource |
|---|---|---|
| Zenodo | One record (get_record) - multiple files |
One entry in record.files[] with a download URL. Requires a non-empty files list |
| CKAN | One package (package_show) - multiple resources |
One entry in package.resources[] with a URL. Skips resources without a URL |
The mapper builds dataset-level fields once (_ZenodoDatasetContext / _CkanDatasetContext), loops over mappable files or resources and calls build_resource per item, then wraps the resulting list[OemetadataResource] in build_dataset_document.
flowchart TB
subgraph source [One upstream dataset]
ZenodoRec["Zenodo record"]
CkanPkg["CKAN package"]
end
subgraph items [Many attachable files]
ZenodoFiles["record.files array"]
CkanRes["package.resources array"]
end
subgraph loop [Mapper loop]
BuildEach["_build_resource_from_file or _build_resource_from_ckan_resource"]
end
subgraph oem [OEMetadata model]
ResList["list of OemetadataResource"]
DatasetDoc["one OEMetadata dataset dict"]
end
ZenodoRec --> ZenodoFiles
CkanPkg --> CkanRes
ZenodoFiles -->|"one per file with URL"| BuildEach
CkanRes -->|"one per resource with URL"| BuildEach
BuildEach --> ResList
ResList -->|"build_dataset_document"| DatasetDoc
Once per map_to_oemetadata, before the per-file (Zenodo) / per-resource (CKAN) loop: _build_dataset_context loads licenses, contributors, title, dates, and keywords shared by every file/resource.
flowchart LR
Src["Zenodo record or CKAN package"]
LicMod["zenodo/licenses or ckan/licenses"]
ContribMod["zenodo/contributors or ckan/contributors"]
Ctx["_DatasetContext"]
Src --> LicMod --> OemLic["list OemetadataLicense"]
Src --> ContribMod --> OemContrib["list OemetadataContributor"]
OemLic --> Ctx
OemContrib --> Ctx
For each mappable file (Zenodo) or resource (CKAN): stream a sample, infer schema and dialect, then build_resource → one OemetadataResource.
flowchart LR
Ctx["_DatasetContext"]
Inf["SchemaInference"]
Br["build_resource"]
Res["OemetadataResource"]
Ctx -->|"licenses, contributors, title, dates"| Br
Inf -->|"schema dict + dialect"| Br
Br --> Res
Inside build_resource, called once per Zenodo file or CKAN resource. OepDefaults (from source config and CLI flags) is passed in on every call.
- Context —
wedowind_context(oep)→OemetadataContext(publisher fromOepDefaults) - Dialect — schema-inference dialect →
OemetadataDialect - Provenance (optional) — if
file_urlis set →OemetadataSource(source_licensesreuses the Phase 1 license list) - Assemble →
OemetadataResource(steps 1–3, Phase 1 licenses/contributors,schema,table_name,pathfromoep.topic, plustopics/languages/subjectfromOepDefaults)
flowchart TD
OepDef["OepDefaults"]
S1["OemetadataContext"]
S2["OemetadataDialect"]
S3["OemetadataSource if file_url"]
S4["OemetadataResource"]
OepDef -->|"wedowind_context"| S1
OepDef -->|"topics, languages, subject, path topic"| S4
S1 --> S2 --> S3 --> S4
OEMetadata defines resources[].licenses (this OEP table) and sources[].sourceLicenses (the upstream file). Zenodo and CKAN expose one license per record/package, so wedowind uses the same Phase 1 list for both.
| Field / child object | Source |
|---|---|
name |
table_name from mapper (OepTable.build_oep_table_name) |
path |
oep_resource_path(oep.topic, table_name) |
context |
OemetadataContext |
dialect |
OemetadataDialect |
sources[] |
optional OemetadataSource from file_url |
licenses |
OemetadataLicense list on resource; same list as source_licenses |
contributors |
Phase 1 — OemetadataContributor list |
schema |
Phase 2 — schema inference for this file, or empty placeholder |
title, description, publication_date, keywords |
Mapper args (often from dataset context) |
topics, languages, subject |
OepDefaults |
After the loop: build_dataset_document runs to_dict() on each resource and adds dataset-level fields and metaMetadata.
flowchart LR
ResList["list OemetadataResource"]
ToDict["resource.to_dict per item"]
BrDoc["build_dataset_document"]
Dict["OEMetadata JSON dict"]
ResList --> ToDict --> BrDoc
BrDoc --> Dict
Then validate_oemetadata and publish_to_oep (see Pipeline diagram above).
How each mapper reaches Zenodo or CKAN:
flowchart TB
subgraph map [Mapping layer]
ZenodoMapper["ZenodoToOepMapper"]
CkanMapper["CKANToOepMapper"]
end
subgraph source [Source layer]
ZenodoClient["ZenodoClient"]
RemoteCKAN["RemoteCKAN"]
ZenodoLic["zenodo/licenses + contributors"]
CkanLic["ckan/licenses + contributors"]
CkanHelpers["ckan/helpers"]
end
subgraph external [External systems]
ZenodoAPI["Zenodo API"]
CkanAPI["CKAN API"]
end
ZenodoMapper --> ZenodoClient
ZenodoMapper --> ZenodoLic
CkanMapper --> RemoteCKAN
CkanMapper --> CkanLic
CkanMapper --> CkanHelpers
ZenodoClient -->|"get_record, file URLs"| ZenodoAPI
RemoteCKAN -->|"package_show, resource URLs"| CkanAPI
ZenodoMapper -->|"schema sample stream"| ZenodoAPI
CkanMapper -->|"schema sample stream"| CkanAPI
See README_incompatibilities.md for field mapping gaps and limitations.
- Zenodo:
src/mappers/zenodo/config/sources.json,src/mappers/zenodo/README.md - CKAN:
src/mappers/ckan/config/sources.json,src/mappers/ckan/README.md
OEP defaults (topic, table_prefix, infer_schema, …) live under defaults.oep (Zenodo) or per-source oep (CKAN).
Monthly workflow: .github/workflows/oep-publish.yml
| Flag | Effect |
|---|---|
--dry-run |
Map + validate; log table provisioning; no OEP writes |
--no-infer-schema |
Skip source file sampling |
--no-provision-tables |
Skip empty table creation |
--schema-sample-lines N |
Lines to stream per source file (default 2) |
--skip-validation |
Skip OMI JSON Schema check |