The data models wrap the output of FullTextXMLParser in typed entity classes that you can normalise, validate, export as dictionaries and convert to RDF. This page covers the entity classes, the builder that creates them from a parsed article, RDFMapper and its mapping file, the conversion scripts, SPARQL queries and SHACL validation.
The entity classes, the builder and RDFMapper are part of the base package, and rdflib is a core dependency. RDFMapper also imports PyYAML, which pyeuropepmc does not declare as a dependency. If from pyeuropepmc.mappers import RDFMapper fails with ModuleNotFoundError: No module named 'yaml', run pip install pyyaml.
The mapping files ship with the package, in src/pyeuropepmc/conf/; the SHACL shapes are in the repository only:
| File | Used by |
|---|---|
rdf_map.yml |
RDFMapper and PaperProcessingPipeline |
rml_mappings.ttl, rdfizer_config.ini |
RMLRDFizer, see RML mappings |
pyeuropepmc-vocab.ttl |
Definitions of the pyeuropepmc: vocabulary |
shacl/pub.shacl.ttl |
SHACL shapes |
RDFMapper() and PaperProcessingPipeline use the packaged rdf_map.yml; pyeuropepmc.conf.config_file("rdf_map.yml") returns its path, for example to copy it as the starting point of your own mapping, which you then pass as RDFMapper(config_path=...).
Fetch an article, build its entities and write them as Turtle:
from rdflib import Graph
from pyeuropepmc import FullTextClient
from pyeuropepmc.builders import build_paper_entities
from pyeuropepmc.features.fulltext.fulltext_parser import FullTextXMLParser
from pyeuropepmc.mappers import RDFMapper
with FullTextClient() as client:
xml_content = client.get_fulltext_content("PMC3258128", format_type="xml")
parser = FullTextXMLParser(xml_content)
paper, authors, sections, tables, figures, references = build_paper_entities(parser)
print(paper.title)
print(f"{len(authors)} authors, {len(sections)} sections, {len(references)} references")
paper.normalize()
paper.validate()
mapper = RDFMapper()
g = Graph()
paper.to_rdf(
g,
mapper=mapper,
related_entities={
"authors": authors,
"sections": sections,
"tables": tables,
"figures": figures,
"references": references,
},
)
print(len(g), "triples")
mapper.serialize_graph(g, format="turtle", destination="PMC3258128.ttl")
For JSON, use to_dict():
import json
with open("PMC3258128.json", "w", encoding="utf-8") as fh:
json.dump(paper.to_dict(), fh, indent=2, default=str)
The entity classes are dataclasses in pyeuropepmc.models. Every field has a default, so you can pass any subset as keyword arguments.
BaseEntity
├── ScholarlyWorkEntity
│ ├── PaperEntity bibo:AcademicArticle
│ └── ReferenceEntity bibo:Document
├── AuthorEntity foaf:Person
├── InstitutionEntity org:Organization
├── JournalEntity bibo:Journal
├── GrantEntity frapo:Grant
├── SectionEntity bibo:DocumentPart, nif:Context
├── TableEntity bibo:Table
├── TableRowEntity bibo:Row
├── FigureEntity bibo:Image
└── AnnotationEntity oa:Annotation, pyeuropepmc:Annotation
├── EntityAnnotation oa:Annotation, oa:SpecificResource, pyeuropepmc:EntityAnnotation
└── RelationshipAnnotation oa:Annotation, rdf:Statement, pyeuropepmc:RelationshipAnnotation
The names after each class are its default types, which become the entity’s rdf:type triples.
pyeuropepmc.models also exports MeSHHeadingEntity and MeSHQualifierEntity, plain dataclasses described in MeSH models and PICO. It also exports the pydantic models LiteratureResult (also available as NormalizedWork), LiteratureSearchResponse, Author, ClinicalTrial and ICiteMetrics, which multi-source search uses.
Every entity has these fields from BaseEntity:
| Field | Type | Default | Description |
|---|---|---|---|
id |
str \| None |
None |
Local identifier |
label |
str \| None |
None |
Human-readable label |
source_uri |
str \| None |
None |
URI of the source record, for example urn:pmc:1234567 |
confidence |
float \| None |
None |
Extraction confidence |
types |
list[str] |
class-specific | RDF types as prefixed names |
data_sources |
list[str] |
[] |
Sources merged into the entity |
last_updated |
str \| None |
None |
Time of the last merge |
| Method | Returns | Description |
|---|---|---|
to_dict() |
dict |
The fields as a dictionary |
to_rdf(g, uri=None, mapper=None, related_entities=None, extraction_info=None, parent_uri=None) |
rdflib.URIRef |
Adds the entity’s triples to graph g and returns its subject URI. Raises ValueError without mapper. |
normalize() |
None |
Cleans values in place, for example trimming whitespace and lower-casing DOIs |
validate() |
None |
Raises ValueError when required fields are missing |
merge_from_source(source_data, source_name) |
None |
Copies values from a dict and records the source |
mint_uri(path) |
rdflib.URIRef |
Returns http://example.org/data/<path>/<id>, assigning a UUID id first if it is None. to_rdf() does not use this URI. |
from pyeuropepmc.models import BaseEntity
entity = BaseEntity(
id="unique-id",
label="Human-readable label",
source_uri="urn:pmc:1234567",
confidence=0.95,
types=["bibo:Document"],
)
print(entity.mint_uri("entity")) # http://example.org/data/entity/unique-id
print(entity.to_dict()["label"])
PaperEntity has 60 fields; dataclasses.fields(PaperEntity) lists them all. The most used:
| Field | Type | Description |
|---|---|---|
title, abstract |
str \| None |
Title and abstract |
doi, pmid, pmcid |
str \| None |
Identifiers |
journal |
JournalEntity \| None |
Journal |
volume |
int \| str \| None |
Volume |
issue, pages, pub_date |
str \| None |
Issue, pages, publication date |
publication_year |
int \| None |
Publication year |
keywords, mesh_terms |
list |
Keywords and MeSH terms, default [] |
citation_count, cited_by_count, influential_citation_count |
int \| None |
Citation counts |
is_oa, oa_status, oa_url |
bool \| None, str \| None, str \| None |
Open-access status |
license |
dict \| None |
Licence information |
grants |
list[GrantEntity] \| None |
Funding |
normalize() lower-cases the DOI and removes a https://doi.org/ prefix. validate() requires at least one of pmcid, doi or title.
from pyeuropepmc.models import JournalEntity, PaperEntity
paper = PaperEntity(
pmcid="PMC1234567",
doi="https://doi.org/10.1234/Example.2024.001",
title="Example Scientific Article",
journal=JournalEntity(title="Nature"),
volume="580",
issue="7805",
pages="123-127",
pub_date="2024-01-15",
keywords=["bioinformatics", "RDF"],
)
paper.normalize()
print(paper.doi) # 10.1234/example.2024.001
paper.validate()
| Field | Type | Description |
|---|---|---|
full_name |
str |
Full name, default "" |
first_name, last_name, initials |
str \| None |
Name parts |
orcid |
str \| None |
ORCID iD |
affiliation_text |
str \| None |
Affiliations as text |
institutions |
list[InstitutionEntity] \| None |
Structured affiliations |
name, email, position |
str \| None |
Alternative name, email, author position |
openalex_id, semantic_scholar_id |
str \| None |
External author IDs |
validate() requires full_name or name.
from pyeuropepmc.models import AuthorEntity
author = AuthorEntity(
full_name="Jane Doe",
first_name="Jane",
last_name="Doe",
initials="J.D.",
orcid="0000-0001-2345-6789",
affiliation_text="University of Example",
)
author.normalize()
author.validate()
| Class | Fields |
|---|---|
SectionEntity |
title, content (str \| None); begin_index, end_index (int \| None, character offsets). validate() requires content. |
TableEntity |
table_label, caption (str \| None); headers (list[str]); rows (list[TableRowEntity]) |
TableRowEntity |
cells (list[str]). Column headers belong to TableEntity.headers. |
FigureEntity |
figure_label, caption, graphic_uri (str \| None). graphic_uri may be an absolute URI or a relative reference such as the file name in <graphic xlink:href="gkr715f1"/>; normalize() and validate() reject only malformed absolute URIs. |
from pyeuropepmc.models import FigureEntity, SectionEntity, TableEntity, TableRowEntity
section = SectionEntity(title="Introduction", content="This is the introduction text...", begin_index=0, end_index=33)
section.validate()
row = TableRowEntity(cells=["Value 1", "Value 2"])
table = TableEntity(table_label="Table 1", caption="Sample data table", headers=["Column 1", "Column 2"], rows=[row])
figure = FigureEntity(figure_label="Figure 1", caption="Study design", graphic_uri="https://example.org/figure1.png")
ReferenceEntity has the ScholarlyWorkEntity fields title, doi, pmid, pmcid, volume, pages, publication_year (int), publication_date, journal, authors (a string or a list of dicts), abstract and citation_count, plus raw_citation.
from pyeuropepmc.models import ReferenceEntity
ref = ReferenceEntity(
title="Referenced Article",
journal="Nature",
publication_year=2023,
volume="600",
pages="123-127",
doi="10.1038/nature12345",
authors="Smith J, Doe J",
)
ref.normalize()
ref.validate()
| Class | Main fields |
|---|---|
InstitutionEntity |
display_name, ror_id, openalex_id, country, country_code, city, latitude, longitude, institution_type, website, established |
JournalEntity |
title, medline_abbreviation, iso_abbreviation, nlmid, issn, essn, publisher, country, language |
GrantEntity |
fundref_doi, funding_source, award_id, recipients (list[AuthorEntity] \| None) |
build_paper_entities(parser, search_data=None) from pyeuropepmc.builders returns a tuple of six values:
| Position | Value | Built from |
|---|---|---|
| 0 | PaperEntity |
parser.extract_metadata(): identifiers, title, journal (as JournalEntity), volume, issue, pages, publication date, keywords, grants |
| 1 | list[AuthorEntity] |
parser.extract_authors_detailed() and parser.extract_affiliations(); affiliations become InstitutionEntity objects |
| 2 | list[SectionEntity] |
parser.get_full_text_sections(): title and text of each section |
| 3 | list[TableEntity] |
parser.extract_tables(), one TableRowEntity per row |
| 4 | list[FigureEntity] |
parser.extract_figures(): label, caption and the <graphic xlink:href> file name as graphic_uri |
| 5 | list[ReferenceEntity] |
parser.extract_references() |
search_data is an optional Europe PMC search result record, one dict from response["resultList"]["result"]. Its PMID, citation count, open-access flags, publication year and similar fields are merged into the paper with merge_from_source(..., "europe_pmc_search").
Some sections have no text and fail SectionEntity.validate(); check section.content before validating sections.
RDFMapper(config_path=None, enable_named_graphs=True) loads the YAML mapping. A missing file raises FileNotFoundError: RDF mapping config not found: <path>.
| Method | Returns | Description |
|---|---|---|
serialize_graph(g, format="turtle", destination=None) |
str |
Serialises a graph in an rdflib format such as turtle, nt, xml or json-ld. With destination, writes the file and returns an empty string. |
convert_and_save_papers_to_rdf(papers_dict, output_dir="rdf_output", prefix="", extraction_info=None, include_content=None) |
dict[str, Graph] |
Converts {identifier: (paper, authors, sections, tables, figures, references)} and writes <output_dir>/<prefix>paper_<identifier>.ttl |
save_rdf(entities_data, output_dir="rdf_output", kg_type=None, extraction_info=None) |
dict[str, Graph] |
Converts {identifier: {"entity": paper, "related_entities": {...}}} for the chosen knowledge-graph type; None uses the default_type in rdf_map.yml (complete) |
save_complete_rdf(...), save_metadata_rdf(...), save_content_rdf(...) |
dict[str, Graph] |
Fixed knowledge-graph type, with file prefixes "", "metadata_" and "content_" |
kg_type |
Entities |
|---|---|
complete |
The paper and all related entities |
metadata |
Paper, authors and institutions |
content |
Sections, references, tables and figures |
from pyeuropepmc.builders import build_paper_entities
from pyeuropepmc.features.fulltext.fulltext_parser import FullTextXMLParser
from pyeuropepmc.mappers import RDFMapper
with open("PMC3258128.xml", encoding="utf-8") as fh:
parser = FullTextXMLParser(fh.read())
paper, authors, sections, tables, figures, references = build_paper_entities(parser)
mapper = RDFMapper()
entities_data = {
"PMC3258128": {
"entity": paper,
"related_entities": {
"authors": authors,
"sections": sections,
"tables": tables,
"figures": figures,
"references": references,
},
}
}
graphs = mapper.save_rdf(entities_data, output_dir="kg_metadata", kg_type="metadata")
print(len(graphs["PMC3258128"]), "triples") # kg_metadata/metadata_paper_PMC3258128.ttl
to_rdf() gets the subject URI from the mapper. For each class the first available identifier wins:
| Entity | URI |
|---|---|
PaperEntity |
https://pubmed.ncbi.nlm.nih.gov/<pmid>/, then https://doi.org/<doi>, then https://www.ncbi.nlm.nih.gov/pmc/articles/<pmcid>/ |
ReferenceEntity |
https://pubmed.ncbi.nlm.nih.gov/<pmid>/, then the PMC URL, then https://doi.org/<doi>, then a https://w3id.org/pyeuropepmc/reference/... URI built from the first author, the year and a hash of the title |
AuthorEntity |
https://w3id.org/pyeuropepmc/author/<normalised name>, then https://orcid.org/<orcid>, then the OpenAlex ID |
InstitutionEntity |
The ROR ID, then the OpenAlex ID, then https://w3id.org/pyeuropepmc/institution/<name> |
JournalEntity |
https://w3id.org/pyeuropepmc/journal/<abbreviation or title> |
SectionEntity |
https://w3id.org/pyeuropepmc/section/<title>, with the paper’s identifier inserted when converted as part of a paper |
| Other entities | A URI under https://w3id.org/pyeuropepmc/, for example .../table/table-1, or one ending in a UUID |
The base https://w3id.org/pyeuropepmc/ is _base_uri in rdf_map.yml. Converted entities also get prov:generatedAtTime and prov:wasGeneratedBy triples, and papers and authors get owl:sameAs links to their DOI, PMC, Europe PMC and ORCID pages.
For each class, rdf_map.yml defines fields (one triple per value), multi_value_fields (one triple per list item) and relationships (links to related entities). A class also uses the fields of its parent classes. The main predicates:
PaperEntity (including ScholarlyWorkEntity fields)
| Field | Predicate |
|---|---|
title |
dcterms:title |
abstract |
dcterms:abstract |
doi |
bibo:doi |
pmcid, pmid |
pyeuropepmc:pmcid, pyeuropepmc:pmid |
volume, issue, pages |
bibo:volume, bibo:issue, bibo:pages |
pub_date, publication_year |
dcterms:issued |
publication_date |
dcterms:date |
citation_count, cited_by_count |
pyeuropepmc:citationCount |
keywords, fields_of_study |
dcterms:subject |
Relationship authors |
dcterms:creator (inverse foaf:made) |
Relationship journal |
bibo:journal |
Relationships sections, tables, figures |
dcterms:hasPart |
Relationship references |
cito:cites |
Relationship grants |
foaf:fundedBy |
AuthorEntity
| Field | Predicate |
|---|---|
full_name |
foaf:name |
first_name, last_name |
foaf:givenName, foaf:familyName |
initials |
foaf:initials |
orcid |
datacite:orcid, plus owl:sameAs to https://orcid.org/<orcid> |
email |
foaf:mbox |
Relationship institutions |
pyeuropepmc:affiliatedWith |
affiliation_text is not mapped; affiliations reach the graph through institutions.
Other classes
| Class | Field | Predicate |
|---|---|---|
SectionEntity |
title, content, begin_index, end_index |
dcterms:title, nif:isString, nif:beginIndex, nif:endIndex |
TableEntity |
caption, table_label, relationship rows |
dcterms:description, rdfs:label, dcterms:hasPart |
TableRowEntity |
cells |
rdfs:member, one triple per cell |
FigureEntity |
caption, figure_label, graphic_uri |
dcterms:description, rdfs:label, foaf:depiction |
ReferenceEntity |
title, doi, volume, pages, publication_year |
dcterms:title, bibo:doi, bibo:volume, bibo:pages, dcterms:issued |
ReferenceEntity |
authors, raw_citation |
bibo:authorList, dcterms:description |
TableEntity.headers is not mapped. The graph types each entity from its types field; the "rdf:type" entries of rdf_map.yml hold the same types and are used for the generated RML file.
Namespaces
| Prefix | Namespace |
|---|---|
bibo |
http://purl.org/ontology/bibo/ |
dcterms |
http://purl.org/dc/terms/ |
foaf |
http://xmlns.com/foaf/0.1/ |
prov |
http://www.w3.org/ns/prov# |
nif |
http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core# |
cito |
http://purl.org/spar/cito/ |
datacite |
http://purl.org/spar/datacite/ |
frapo |
http://purl.org/cerif/frapo/ |
org |
http://www.w3.org/ns/org# |
oa |
http://www.w3.org/ns/oa# |
pyeuropepmc |
https://w3id.org/pyeuropepmc/vocab# |
rdf_map.yml also declares owl, rdfs, rdf, skos, geo, mesh, meshv, obo, ror, xsd and pyeuropepmcdata.
Copy rdf_map.yml, change the predicates, and pass the copy as config_path. A class entry looks like this:
PaperEntity:
"rdf:type": ["bibo:AcademicArticle"]
fields:
issue: {predicate: "bibo:issue", datatype: "xsd:string"}
abstract: {predicate: "dcterms:abstract", datatype: "xsd:string"}
multi_value_fields:
keywords: dcterms:subject
relationships:
authors:
predicate: "dcterms:creator"
inverse: foaf:made
RDFMapper reads the YAML file directly. The RML file rml_mappings.ttl is generated from it by examples/scripts/sync_rdf_mappings.py (options --yaml and --rml), also run by make sync-rdf. A field may map to {predicate, datatype} or, as the annotation classes do, to a bare predicate string. "rdf:type" only affects the generated RML file: RDFMapper takes the types from the entity’s types field, so change both when you change a type.
To add a field in Python, subclass an entity and decorate the subclass with @dataclass. Without the decorator, the new attribute is not a dataclass field and to_dict() leaves it out.
from dataclasses import dataclass
from pyeuropepmc.models import PaperEntity
@dataclass
class ExtendedPaperEntity(PaperEntity):
custom_field: str = ""
extended = ExtendedPaperEntity(title="Example", custom_field="value")
print(extended.to_dict()["custom_field"]) # value
pyeuropepmc.mappers also has functions that convert data directly to an rdflib Dataset. Each also accepts the optional parameters config_path, namespaces, cache_backend and extraction_info.
| Function | Converts |
|---|---|
convert_xml_to_rdf(xml_data, ..., include_content=True) |
Parsed XML data (dict) |
convert_search_to_rdf(search_results, ...) |
Europe PMC search results |
convert_enrichment_to_rdf(enrichment_data, ...) |
Enrichment results |
convert_annotations_to_rdf(annotations_data, ...) |
Europe PMC Annotations API responses |
convert_pipeline_to_rdf(search_results=None, xml_data=None, enrichment_data=None, annotations_data=None, ..., include_content=True) |
Any combination of the above |
convert_incremental_to_rdf(base_rdf, enrichment_data, ...) |
Adds enrichment data to an existing Graph and returns the Graph |
A source checkout includes two conversion scripts in examples/scripts/. They use the mapping files that ship with the package unless you pass other paths.
python examples/scripts/xml_to_rdf.py PMC3258128.xml --ttl PMC3258128.ttl --json PMC3258128.json -v
python examples/scripts/xml_to_rdf_rml.py PMC3258128.xml --output PMC3258128_rml.ttl --json entities.json -v
| Script | Options |
|---|---|
xml_to_rdf.py INPUT |
--ttl PATH (Turtle output), --json PATH (entities as JSON), --config PATH (mapping YAML), -v/--verbose |
xml_to_rdf_rml.py INPUT |
--output/-o PATH (required, Turtle output), --json PATH (entities as JSON), --mappings PATH (RML file), --config PATH (RDFizer configuration), -v/--verbose |
from rdflib import Graph
g = Graph()
g.parse("PMC3258128.ttl", format="turtle")
query = """
PREFIX bibo: <http://purl.org/ontology/bibo/>
PREFIX dcterms: <http://purl.org/dc/terms/>
SELECT ?paper ?title ?doi
WHERE {
?paper a bibo:AcademicArticle ;
dcterms:title ?title .
OPTIONAL { ?paper bibo:doi ?doi }
}
"""
for row in g.query(query):
print(row.paper, row.title, row.doi)
shacl/pub.shacl.ttl defines shapes for bibo:AcademicArticle, foaf:Person, bibo:DocumentPart, bibo:Table and bibo:Document, mostly limits on how often a property may occur. Validating against them needs pyshacl, which is not installed with pyeuropepmc:
pip install pyshacl
from pyshacl import validate
from rdflib import Graph
data_graph = Graph().parse("PMC3258128.ttl", format="turtle")
shapes_graph = Graph().parse("pub.shacl.ttl", format="turtle")
conforms, results_graph, results_text = validate(data_graph, shacl_graph=shapes_graph, inference="rdfs")
print(f"Conforms: {conforms}")
if not conforms:
print(results_text)