Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

ODRL Policy Nanopublication Generator

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:

  1. Go to Nanodash → Publish → Assertion Template

  2. Fill in:

    • Name: ODRL Access Policy

    • Description: Defines an ODRL policy for controlling access to a FAIR dataset. Specifies permissions (with purpose constraints), prohibitions, and attribution duties.

    • Tag: FAIR2Adapt

  3. Define the statements (see the assertion structure in the generated TriG files)

  4. Publish the template and copy its URI

  5. Update ODRL_POLICY_TEMPLATE below with the URI



📝 SECTION 1: INPUT FILE (EDIT THIS)


# Path to your ODRL policy JSON config file
CONFIG_FILE = "config/hamburg_odrl_policy.json"

OUTPUT_DIR = "output/"

⚙️ SECTION 2: SETUP


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

📖 SECTION 3: LOAD & VALIDATE


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)

🔨 SECTION 4: BUILD NANOPUBLICATIONS


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

📄 SECTION 5: PREVIEW


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 .
}



🚀 SECTION 6: SIGN & PUBLISH


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 path
if 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"
          }
        }
      ]
    }
  ]
}