Creates ODRL (Open Digital Rights Language) policy nanopublications from a JSON configuration file.
Each nanopub encodes an access policy for a FAIR2Adapt dataset, specifying:
Permissions: what actions are allowed (e.g., use, reproduce) and under what constraints (e.g., academic research only)
Prohibitions: what actions are not allowed (e.g., distribute, commercialize)
Duties: what obligations must be met (e.g., attribution)
Template: To be created on Nanodash — see instructions below.
How to create the ODRL Policy template on Nanodash¶
Before using this notebook, create an assertion template on Nanodash:
Fill in:
Name:
ODRL Access PolicyDescription:
Defines an ODRL policy for controlling access to a FAIR dataset. Specifies permissions (with purpose constraints), prohibitions, and attribution duties.Tag:
FAIR2Adapt
Define the statements (see the assertion structure in the generated TriG files)
Publish the template and copy its URI
Update
ODRL_POLICY_TEMPLATEbelow with the URI
# Path to your ODRL policy JSON config file
CONFIG_FILE = "config/hamburg_odrl_policy.json"
OUTPUT_DIR = "output/"import json
from pathlib import Path
from datetime import datetime, timezone
from rdflib import Dataset, Namespace, URIRef, Literal, BNode
from rdflib.namespace import RDF, RDFS, XSD, FOAF
# Namespaces
NP = Namespace("http://www.nanopub.org/nschema#")
DCT = Namespace("http://purl.org/dc/terms/")
NT = Namespace("https://w3id.org/np/o/ntemplate/")
NPX = Namespace("http://purl.org/nanopub/x/")
PROV = Namespace("http://www.w3.org/ns/prov#")
ORCID = Namespace("https://orcid.org/")
ODRL = Namespace("http://www.w3.org/ns/odrl/2/")
DPV = Namespace("https://w3id.org/dpv#")
# Template URIs (v2 — with GroupedStatement)
ODRL_POLICY_TEMPLATE = URIRef("https://w3id.org/np/RA61D4c7dB5t0B1mLhc78bN2vagqYTXQiJDKY0yImRULI")
PROV_TEMPLATE = URIRef("https://w3id.org/np/RA7lSq6MuK_TIC6JMSHvLtee3lpLoZDOqLJCLXevnrPoU")
PUBINFO_TEMPLATE_1 = URIRef("https://w3id.org/np/RA0J4vUn_dekg-U1kK3AOEt02p9mT2WO03uGxLDec1jLw")
PUBINFO_TEMPLATE_2 = URIRef("https://w3id.org/np/RAukAcWHRDlkqxk7H2XNSegc1WnHI569INvNr-xdptDGI")
# ODRL action URIs
ODRL_ACTIONS = {
"use": ODRL.use,
"reproduce": ODRL.reproduce,
"distribute": ODRL.distribute,
"commercialize": ODRL.commercialize,
"derive": ODRL.derive,
"modify": ODRL.modify,
"attribute": ODRL.attribute,
"present": ODRL.present,
"sell": ODRL.sell,
"transfer": ODRL.transfer,
}
# Purpose URIs — W3C Data Privacy Vocabulary (DPV)
PURPOSE_URIS = {
"AcademicResearch": DPV.AcademicResearch,
"ScientificResearch": DPV.ScientificResearch,
"NonCommercialResearch": DPV.NonCommercialResearch,
"PublicBenefit": DPV.PublicBenefit,
}
print("Setup complete")Setup complete
print(f"Loading: {CONFIG_FILE}")
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
config = json.load(f)
metadata = config.get('metadata', {})
AUTHOR_ORCID = metadata.get('creator_orcid')
AUTHOR_NAME = metadata.get('creator_name')
errors = []
if not AUTHOR_ORCID:
errors.append("metadata.creator_orcid is required")
if not AUTHOR_NAME:
errors.append("metadata.creator_name is required")
if not config.get('nanopublications'):
errors.append("nanopublications list is required")
for i, np_config in enumerate(config.get('nanopublications', [])):
if not np_config.get('policy_uid'):
errors.append(f"nanopublications[{i}].policy_uid is required")
if not np_config.get('target', {}).get('uri'):
errors.append(f"nanopublications[{i}].target.uri is required")
if errors:
print("❌ Validation errors:")
for e in errors:
print(f" - {e}")
raise ValueError("Please fix the errors in your JSON file")
print(f"✓ Loaded {len(config['nanopublications'])} ODRL policy nanopubs to generate")
print(f"✓ Author: {AUTHOR_NAME} ({AUTHOR_ORCID})")Loading: config/hamburg_odrl_policy.json
✓ Loaded 3 ODRL policy nanopubs to generate
✓ Author: Anne Fouilloux (0000-0002-1784-2920)
def create_odrl_policy_nanopub(np_config, metadata):
"""
Create an ODRL policy nanopublication matching the v2 template structure.
Template structure:
- policy rdf:type Offer/Set
- policy odrl:target dataset (top-level, not on blank nodes)
- policy odrl:permission _:perm (one per action, repeatable)
- _:perm odrl:action <action>
- _:perm odrl:constraint _:const (optional)
- _:const odrl:leftOperand purpose
- _:const odrl:operator eq
- _:const odrl:rightOperand <purpose>
- policy odrl:prohibition _:prohib (one per action, repeatable)
- _:prohib odrl:action <action>
- policy odrl:duty _:duty (optional)
- _:duty odrl:action <action>
- _:duty odrl:attributedParty <uri>
"""
TEMP_NP = Namespace("http://purl.org/nanopub/temp/np/")
this_np = URIRef("http://purl.org/nanopub/temp/np/")
head_graph = URIRef("http://purl.org/nanopub/temp/np/Head")
assertion_graph = URIRef("http://purl.org/nanopub/temp/np/assertion")
provenance_graph = URIRef("http://purl.org/nanopub/temp/np/provenance")
pubinfo_graph = URIRef("http://purl.org/nanopub/temp/np/pubinfo")
author_uri = ORCID[metadata['creator_orcid']]
policy_uri = URIRef(np_config['policy_uid'])
target_uri = URIRef(np_config['target']['uri'])
policy_type = np_config.get('policy_type', 'Offer')
ds = Dataset()
# Bind prefixes
ds.bind("this", "http://purl.org/nanopub/temp/np/")
ds.bind("sub", TEMP_NP)
ds.bind("np", NP)
ds.bind("dct", DCT)
ds.bind("nt", NT)
ds.bind("npx", NPX)
ds.bind("xsd", XSD)
ds.bind("rdfs", RDFS)
ds.bind("orcid", ORCID)
ds.bind("prov", PROV)
ds.bind("foaf", FOAF)
ds.bind("odrl", ODRL)
ds.bind("dpv", DPV)
# HEAD graph
head = ds.graph(head_graph)
head.add((this_np, RDF.type, NP.Nanopublication))
head.add((this_np, NP.hasAssertion, assertion_graph))
head.add((this_np, NP.hasProvenance, provenance_graph))
head.add((this_np, NP.hasPublicationInfo, pubinfo_graph))
# ASSERTION graph — the ODRL policy
assertion = ds.graph(assertion_graph)
# Policy type (no odrl:uid or odrl:profile — not in template)
assertion.add((policy_uri, RDF.type, ODRL[policy_type]))
# Target at policy level (matches template st_target)
assertion.add((policy_uri, ODRL.target, target_uri))
# Permissions — one blank node per action (matches template permGroup repeatable)
for perm in np_config.get('permissions', []):
purpose_val = perm.get('purpose')
for action_name in perm.get('actions', []):
perm_node = BNode()
action_uri = ODRL_ACTIONS.get(action_name, ODRL[action_name])
assertion.add((policy_uri, ODRL.permission, perm_node))
assertion.add((perm_node, ODRL.action, action_uri))
# Purpose constraint (same for all actions in this permission block)
if purpose_val:
constraint_node = BNode()
assertion.add((perm_node, ODRL.constraint, constraint_node))
assertion.add((constraint_node, ODRL.leftOperand, ODRL.purpose))
assertion.add((constraint_node, ODRL.operator, ODRL.eq))
if purpose_val.startswith("http"):
purpose_uri = URIRef(purpose_val)
else:
purpose_uri = PURPOSE_URIS.get(purpose_val, DPV[purpose_val])
assertion.add((constraint_node, ODRL.rightOperand, purpose_uri))
# Prohibitions — one blank node per action (matches template prohibGroup repeatable)
for prohib in np_config.get('prohibitions', []):
for action_name in prohib.get('actions', []):
prohib_node = BNode()
action_uri = ODRL_ACTIONS.get(action_name, ODRL[action_name])
assertion.add((policy_uri, ODRL.prohibition, prohib_node))
assertion.add((prohib_node, ODRL.action, action_uri))
# Duties (matches template dutyGroup)
for duty in np_config.get('duties', []):
duty_node = BNode()
action_uri = ODRL_ACTIONS.get(duty['action'], ODRL[duty['action']])
assertion.add((policy_uri, ODRL.duty, duty_node))
assertion.add((duty_node, ODRL.action, action_uri))
if duty.get('attributed_party', {}).get('uri'):
assertion.add((duty_node, ODRL.attributedParty,
URIRef(duty['attributed_party']['uri'])))
# PROVENANCE graph
provenance = ds.graph(provenance_graph)
provenance.add((assertion_graph, PROV.wasAttributedTo, author_uri))
# PUBINFO graph
pubinfo = ds.graph(pubinfo_graph)
pubinfo.add((author_uri, FOAF.name, Literal(metadata['creator_name'])))
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S+00:00")
pubinfo.add((this_np, DCT.created, Literal(now, datatype=XSD.dateTime)))
pubinfo.add((this_np, DCT.creator, author_uri))
pubinfo.add((this_np, DCT.license, URIRef("https://creativecommons.org/licenses/by/4.0/")))
pubinfo.add((this_np, NPX.wasCreatedAt, URIRef("https://fair2adapt-eosc.eu")))
# Nanopub type
pubinfo.add((this_np, NPX.hasNanopubType, ODRL.Policy))
# Introduces the policy
pubinfo.add((this_np, NPX.introduces, policy_uri))
# Label
target_label = np_config['target'].get('label', np_config['target']['uri'])
label = f"ODRL policy: {target_label}"
pubinfo.add((this_np, RDFS.label, Literal(label)))
# Template references
pubinfo.add((this_np, NT.wasCreatedFromTemplate, ODRL_POLICY_TEMPLATE))
pubinfo.add((this_np, NT.wasCreatedFromProvenanceTemplate, PROV_TEMPLATE))
pubinfo.add((this_np, NT.wasCreatedFromPubinfoTemplate, PUBINFO_TEMPLATE_1))
pubinfo.add((this_np, NT.wasCreatedFromPubinfoTemplate, PUBINFO_TEMPLATE_2))
# Supersede old nanopub if specified
if np_config.get('supersedes'):
pubinfo.add((this_np, NPX.supersedes, URIRef(np_config['supersedes'])))
return ds, label
print("Function defined")Function defined
# Create output directory
Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
# Generate all nanopublications
generated_files = []
for np_config in config['nanopublications']:
ds, label = create_odrl_policy_nanopub(np_config, metadata)
output_file = Path(OUTPUT_DIR) / f"{np_config['id']}.trig"
ds.serialize(destination=str(output_file), format='trig')
generated_files.append(output_file)
print(f"✓ Generated: {output_file}")
print(f"\nTotal generated: {len(generated_files)} nanopublications")✓ Generated: output/odrl_hamburg_buildings.trig
✓ Generated: output/odrl_hamburg_statistical_units.trig
✓ Generated: output/odrl_hamburg_risk_private.trig
Total generated: 3 nanopublications
if generated_files:
print(f"Preview of {generated_files[0]}:\n")
print("=" * 80)
with open(generated_files[0], 'r') as f:
print(f.read())Preview of output/odrl_hamburg_buildings.trig:
================================================================================
@prefix dct: <http://purl.org/dc/terms/> .
@prefix dpv: <https://w3id.org/dpv#> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix np: <http://www.nanopub.org/nschema#> .
@prefix npx: <http://purl.org/nanopub/x/> .
@prefix nt: <https://w3id.org/np/o/ntemplate/> .
@prefix odrl: <http://www.w3.org/ns/odrl/2/> .
@prefix orcid: <https://orcid.org/> .
@prefix prov: <http://www.w3.org/ns/prov#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix sub: <http://purl.org/nanopub/temp/np/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
sub:assertion {
<https://fair2adapt.eu/policy/hamburg-buildings> a odrl:Offer ;
odrl:duty _:N487cb9e25c5d4e9792732db6738c2980 ;
odrl:permission _:N4251e18b2eee490b88cbeb0fc5e8ebf6,
_:Nab5d8bf911a14fd9bbb29fe6eeac5b27 ;
odrl:prohibition _:N2846452f50d944ad9d5c310c74d13f76,
_:N4ce54ed1dc114d78a0e49421468aaab7 ;
odrl:target <https://fair2adapt.eu/data/hamburg-buildings> .
_:N2846452f50d944ad9d5c310c74d13f76 odrl:action odrl:distribute .
_:N4251e18b2eee490b88cbeb0fc5e8ebf6 odrl:action odrl:use ;
odrl:constraint _:Na4f350aedf764ec48ce7962c5223dac3 .
_:N487cb9e25c5d4e9792732db6738c2980 odrl:action odrl:attribute ;
odrl:attributedParty <https://fair2adapt-eosc.eu> .
_:N4ce54ed1dc114d78a0e49421468aaab7 odrl:action odrl:commercialize .
_:Na4f350aedf764ec48ce7962c5223dac3 odrl:leftOperand odrl:purpose ;
odrl:operator odrl:eq ;
odrl:rightOperand dpv:AcademicResearch .
_:Nab5d8bf911a14fd9bbb29fe6eeac5b27 odrl:action odrl:reproduce ;
odrl:constraint _:Nc4cb7013ef9641a2ae5664e1a2294bd2 .
_:Nc4cb7013ef9641a2ae5664e1a2294bd2 odrl:leftOperand odrl:purpose ;
odrl:operator odrl:eq ;
odrl:rightOperand dpv:AcademicResearch .
}
sub:provenance {
sub:assertion prov:wasAttributedTo orcid:0000-0002-1784-2920 .
}
sub:pubinfo {
sub: rdfs:label "ODRL policy: Hamburg building footprints with demographic indicators" ;
dct:created "2026-03-29T15:37:44+00:00"^^xsd:dateTime ;
dct:creator orcid:0000-0002-1784-2920 ;
dct:license <https://creativecommons.org/licenses/by/4.0/> ;
npx:hasNanopubType odrl:Policy ;
npx:introduces <https://fair2adapt.eu/policy/hamburg-buildings> ;
npx:supersedes <https://w3id.org/np/RAgtBCd3TIGKD1LEQfS5fv_4bXLNz_eMZJejX9KEsYp4M> ;
npx:wasCreatedAt <https://fair2adapt-eosc.eu> ;
nt:wasCreatedFromProvenanceTemplate <https://w3id.org/np/RA7lSq6MuK_TIC6JMSHvLtee3lpLoZDOqLJCLXevnrPoU> ;
nt:wasCreatedFromPubinfoTemplate <https://w3id.org/np/RA0J4vUn_dekg-U1kK3AOEt02p9mT2WO03uGxLDec1jLw>,
<https://w3id.org/np/RAukAcWHRDlkqxk7H2XNSegc1WnHI569INvNr-xdptDGI> ;
nt:wasCreatedFromTemplate <https://w3id.org/np/RA61D4c7dB5t0B1mLhc78bN2vagqYTXQiJDKY0yImRULI> .
orcid:0000-0002-1784-2920 foaf:name "Anne Fouilloux" .
}
sub:Head {
sub: a np:Nanopublication ;
np:hasAssertion sub:assertion ;
np:hasProvenance sub:provenance ;
np:hasPublicationInfo sub:pubinfo .
}
PUBLISH = True # Set to True when ready to publish
USE_TEST_SERVER = False # Set to False for production
PROFILE_PATH = "/Users/annef/Documents/ScienceLive/annefou-profile/profile.yml" # Set to your profile.yml pathif PUBLISH:
from nanopub import Nanopub, NanopubConf, load_profile
profile = load_profile(PROFILE_PATH)
print(f"Loaded profile: {profile.name}")
conf = NanopubConf(profile=profile, use_test_server=USE_TEST_SERVER)
published_uris = []
for trig_file in generated_files:
np_obj = Nanopub(rdf=trig_file, conf=conf)
np_obj.sign()
signed_path = trig_file.with_suffix('.signed.trig')
np_obj.store(signed_path)
print(f"✓ Signed: {signed_path}")
np_obj.publish()
published_uris.append(np_obj.source_uri)
print(f"✓ Published: {np_obj.source_uri}")
# Save published URIs for updating registry.json
print("\n" + "=" * 80)
print("Update policies/registry.json with these nanopub URIs:")
print("=" * 80)
for np_config, uri in zip(config['nanopublications'], published_uris):
dataset_id = np_config['id'].replace('odrl_', '').replace('_', '-')
print(f' "{dataset_id}": {{"policy_nanopub": "{uri}"}}')
else:
print("Publishing disabled. Set PUBLISH = True to enable.")
print("\nManual signing:")
for f in generated_files:
print(f" nanopub sign {f}")
print(f" nanopub publish {f.stem}.signed.trig")Loaded profile: Anne Fouilloux
✓ Signed: output/odrl_hamburg_buildings.signed.trig
✓ Published: https://w3id.org/np/RAJr0en7FUjJUG4Iav-BC_npvxcEn18VVRZfu4fQcc978
✓ Signed: output/odrl_hamburg_statistical_units.signed.trig
✓ Published: https://w3id.org/np/RA7ulL1qGsNX9THXbRlow1is6__jbNHYCrr7_8Glp3GJw
✓ Signed: output/odrl_hamburg_risk_private.signed.trig
✓ Published: https://w3id.org/np/RAX98xyu69t1xBlq8yx4DUmoR44-nYHzCl6ZifKYZQAzM
================================================================================
Update policies/registry.json with these nanopub URIs:
================================================================================
"hamburg-buildings": {"policy_nanopub": "https://w3id.org/np/RAJr0en7FUjJUG4Iav-BC_npvxcEn18VVRZfu4fQcc978"}
"hamburg-statistical-units": {"policy_nanopub": "https://w3id.org/np/RA7ulL1qGsNX9THXbRlow1is6__jbNHYCrr7_8Glp3GJw"}
"hamburg-risk-private": {"policy_nanopub": "https://w3id.org/np/RAX98xyu69t1xBlq8yx4DUmoR44-nYHzCl6ZifKYZQAzM"}
📋 JSON Config Template¶
{
"metadata": {
"creator_orcid": "0000-0002-1784-2920",
"creator_name": "Your Name",
"project": {
"uri": "https://fair2adapt-eosc.eu",
"label": "FAIR2Adapt"
}
},
"nanopublications": [
{
"id": "odrl_my_dataset",
"policy_uid": "https://fair2adapt.eu/policy/my-dataset",
"policy_type": "Offer",
"target": {
"uri": "https://fair2adapt.eu/data/my-dataset",
"label": "Description of the dataset"
},
"permissions": [
{
"actions": ["use", "reproduce"],
"purpose": "AcademicResearch"
}
],
"prohibitions": [
{ "actions": ["distribute", "commercialize"] }
],
"duties": [
{
"action": "attribute",
"attributed_party": {
"uri": "https://fair2adapt-eosc.eu",
"label": "FAIR2Adapt"
}
}
]
}
]
}