Repository: pybel/pybel (python)

Issue:
Implement enrich_pubmed_central_citations
Implement enrichment of citations pointing to PubMed Central (e.g., PMCXXXXXX) similar to `pybel.manager.citation_utils.enrich_pubmed_citations`.

- [ ] Implement `get_pubmed_central_identifiers()` like `pybel.struct.summary.provenance.get_pubmed_identifiers()`
- [ ] figure out PubMed Central API. it's probably a lot easier than the PubMed one :p

Pull request description:
Add enrichment for PMC citations
Closes #420... blazin'

Patch:
diff --git a/src/pybel/manager/citation_utils.py b/src/pybel/manager/citation_utils.py
index b62472ff..a3e67e6e 100644
--- a/src/pybel/manager/citation_utils.py
+++ b/src/pybel/manager/citation_utils.py
@@ -4,7 +4,8 @@
 
 import logging
 import re
-from datetime import datetime
+from datetime import date, datetime
+from functools import lru_cache
 from typing import Dict, Iterable, List, Optional, Set, Tuple, Union
 
 import ratelimit
@@ -16,11 +17,11 @@ from tqdm import tqdm
 from . import models
 from ..constants import CITATION
 from ..struct.filters import filter_edges
-from ..struct.filters.edge_predicates import has_pubmed
-from ..struct.summary.provenance import get_pubmed_identifiers
+from ..struct.filters.edge_predicates import CITATION_PREDIACATES
+from ..struct.summary.provenance import get_citation_identifiers
 
 __all__ = [
-    'get_citations_by_pmids',
+    '_get_citations_by_identifiers',
     'enrich_pubmed_citations',
 ]
 
@@ -73,9 +74,10 @@ def sanitize_date(publication_date: str) -> str:
         return datetime.strptime(publication_date, '%Y %b %d-{}'.format(s.groups()[0])).strftime('%Y-%m-%d')
 
 
-def clean_pubmed_identifiers(pmids: Iterable[str]) -> List[str]:
-    """Clean a list of PubMed identifiers with string strips, deduplicates, and sorting."""
-    return sorted({str(pmid).strip() for pmid in pmids})
+def clean_pubmed_identifiers(identifiers: Iterable[str]) -> List[str]:
+    """Clean a list of identifiers with string strips, deduplicates, and sorting."""
+    _identifiers = (str(identifier).strip() for identifier in identifiers if identifier)
+    return sorted({i for i in _identifiers if i})
 
 
 @ratelimit.limits(calls=3, period=1)
@@ -149,64 +151,84 @@ def get_citations_by_pmids(
     *,
     group_size: Optional[int] = None,
     offline: bool = False,
+) -> Tuple[Dict[str, Dict], Set[str]]:
+    return _get_citations_by_identifiers(
+        manager=manager, identifiers=pmids, group_size=group_size, offline=offline, prefix='pubmed',
+    )
+
+
+def _get_citations_by_identifiers(
+    manager,
+    identifiers: Iterable[Union[str, int]],
+    *,
+    group_size: Optional[int] = None,
+    offline: bool = False,
+    prefix: Optional[str] = None,
 ) -> Tuple[Dict[str, Dict], Set[str]]:
     """Get citation information for the given list of PubMed identifiers using the NCBI's eUtils service.
 
     :type manager: pybel.Manager
-    :param pmids: an iterable of PubMed identifiers
+    :param identifiers: an iterable of PubMed identifiers
     :param group_size: The number of PubMed identifiers to query at a time. Defaults to 200 identifiers.
-    :return: A dictionary of {pmid: pmid data dictionary} or a pair of this dictionary and a set ot erroneous
-            pmids if return_errors is :data:`True`
+    :return: A dictionary of {identifier: data dictionary} or a pair of this dictionary and a set ot erroneous
+             identifiers.
     """
+    if prefix is None:
+        prefix = 'pubmed'
+
+    helper = _HELPERS.get(prefix)
+    if helper is None:
+        raise ValueError(f'can not work on prefix: {prefix}')
+
     group_size = group_size if group_size is not None else 200
 
-    pmids = clean_pubmed_identifiers(pmids)
-    logger.info('ensuring %d PubMed identifiers', len(pmids))
+    identifiers = clean_pubmed_identifiers(identifiers)
+    logger.info('ensuring %d %s identifiers', len(identifiers), prefix)
 
-    enriched_pmids = {}
-    unenriched_pmids = {}
+    enriched_models = {}
+    unenriched_models = {}
 
-    pmid_to_model = {
+    id_to_model = {
         citation_model.db_id: citation_model
-        for citation_model in _get_citation_models(pmids, prefix='pubmed', manager=manager)
+        for citation_model in _get_citation_models(identifiers, prefix=prefix, manager=manager)
     }
-    logger.info('%d of %d are already cached', len(pmid_to_model), len(pmids))
-    for pmid in tqdm(pmids, desc='creating database models'):
-        citation = pmid_to_model.get(pmid)
-        if citation is None:
-            citation = pmid_to_model[pmid] = manager.get_or_create_citation(identifier=pmid)
-        if citation.is_enriched:
-            enriched_pmids[pmid] = citation.to_json()
+    logger.info('%d of %d %s identifiers are already cached', len(id_to_model), len(identifiers), prefix)
+    for identifier in tqdm(identifiers, desc=f'creating {prefix} models'):
+        model = id_to_model.get(identifier)
+        if model is None:
+            model = id_to_model[identifier] = manager.get_or_create_citation(identifier=identifier, namespace=prefix)
+        if model.is_enriched:
+            enriched_models[identifier] = model.to_json()
         else:
-            unenriched_pmids[pmid] = citation
+            unenriched_models[identifier] = model
 
-    logger.info('%d of %d are already enriched', len(enriched_pmids), len(pmids))
+    logger.info('%d of %d %s are identifiers already enriched', len(enriched_models), len(identifiers), prefix)
     manager.session.commit()
 
     errors = set()
-    if not unenriched_pmids or offline:
-        return enriched_pmids, errors
+    if not unenriched_models or offline:
+        return enriched_models, errors
 
-    it = tqdm(unenriched_pmids, desc=f'getting PubMed data in chunks of {group_size}')
-    for pmid_list in chunked(it, n=group_size):
-        _help_enrich_pmids(
-            pmid_list,
+    it = tqdm(unenriched_models, desc=f'getting {prefix} data in chunks of {group_size}')
+    for identifier_chunk in chunked(it, n=group_size):
+        helper(
+            identifier_chunk,
             manager=manager,
-            enriched_pmids=enriched_pmids,
-            unenriched_pmids=unenriched_pmids,
+            enriched_models=enriched_models,
+            unenriched_models=unenriched_models,
             errors=errors,
         )
 
-    return enriched_pmids, errors
+    return enriched_models, errors
 
 
-def _help_enrich_pmids(pmid_list, *, manager, unenriched_pmids, enriched_pmids, errors):
-    response = get_pubmed_citation_response(pmid_list)
+def _help_enrich_pmids(identifiers: Iterable[str], *, manager, unenriched_models, enriched_models, errors):
+    response = get_pubmed_citation_response(identifiers)
     response_pmids = response['result']['uids']
 
     for pmid in response_pmids:
         p = response['result'][pmid]
-        citation = unenriched_pmids.get(pmid)
+        citation = unenriched_models.get(pmid)
         if citation is None:
             tqdm.write(f'problem looking up pubmed:{pmid}')
             continue
@@ -214,16 +236,39 @@ def _help_enrich_pmids(pmid_list, *, manager, unenriched_pmids, enriched_pmids,
         successful_enrichment = enrich_citation_model(manager, citation, p)
 
         if not successful_enrichment:
-            tqdm.write(f"Error downloading PubMed identifier: {pmid}")
+            tqdm.write(f"Error downloading pubmed:{pmid}")
             errors.add(pmid)
             continue
 
-        enriched_pmids[pmid] = citation.to_json()
+        enriched_models[pmid] = citation.to_json()
         manager.session.add(citation)
 
     manager.session.commit()  # commit in groups
 
 
+def _help_enrich_pmc_identifiers(identifiers: Iterable[str], *, manager, unenriched_models, enriched_models, errors):
+    for pmcid in identifiers:
+        try:
+            csl = get_pmc_csl_item(pmcid)
+        except Exception:
+            tqdm.write(f"Error downloading pmc:{pmcid}")
+            errors.add(pmcid)
+            continue
+
+        model = unenriched_models[pmcid]
+        enrich_citation_model_from_pmc(manager=manager, citation=model, csl=csl)
+        manager.session.add(model)
+        enriched_models[pmcid] = model.to_json()
+
+    manager.session.commit()  # commit in groups
+
+
+_HELPERS = {
+    'pubmed': _help_enrich_pmids,
+    'pmc': _help_enrich_pmc_identifiers,
+}
+
+
 def _get_citation_models(identifiers: Iterable[str], *, prefix: str, manager, chunksize: int = 200):
     for identifiers_chunk in chunked(identifiers, chunksize):
         citation_filter = and_(
@@ -241,25 +286,108 @@ def enrich_pubmed_citations(
 ) -> Set[str]:
     """Overwrite all PubMed citations with values from NCBI's eUtils lookup service.
 
-    Sets authors as list, so probably a good idea to run :func:`pybel_tools.mutation.serialize_authors` before
-    exporting.
-
     :type manager: pybel.manager.Manager
     :type graph: pybel.BELGraph
     :param group_size: The number of PubMed identifiers to query at a time. Defaults to 200 identifiers.
+    :param offline: An override for when you don't want to hit the eUtils
     :return: A set of PMIDs for which the eUtils service crashed
     """
-    pmids = {x for x in get_pubmed_identifiers(graph) if x}
-    pmid_data, errors = get_citations_by_pmids(manager, pmids=pmids, group_size=group_size, offline=offline)
+    return _enrich_citations(
+        manager=manager, graph=graph, group_size=group_size, offline=offline, prefix='pubmed',
+    )
 
-    for u, v, k in filter_edges(graph, has_pubmed):
-        pmid = graph[u][v][k][CITATION].identifier
 
-        if pmid not in pmid_data:
-            logger.warning('Missing data for PubMed identifier: %s', pmid)
-            errors.add(pmid)
+def _enrich_citations(
+    manager,
+    graph,
+    group_size: Optional[int] = None,
+    offline: bool = False,
+    prefix: Optional[str] = None,
+) -> Set[str]:
+    """Overwrite all citations of the given prefix using the predefined lookup functions.
+
+    :type manager: pybel.manager.Manager
+    :type graph: pybel.BELGraph
+    :param group_size: The number of identifiers to query at a time. Defaults to 200 identifiers.
+    :return: A set of identifiers for which lookup was not possible
+    """
+    if prefix is None:
+        prefix = 'pubmed'
+
+    identifiers = {identifier for identifier in get_citation_identifiers(graph, prefix) if identifier}
+    identifier_map, errors = _get_citations_by_identifiers(
+        manager,
+        identifiers=identifiers,
+        group_size=group_size,
+        offline=offline,
+        prefix=prefix,
+    )
+
+    for u, v, k in filter_edges(graph, CITATION_PREDIACATES[prefix]):
+        identifier = graph[u][v][k][CITATION].identifier
+
+        identifier_data = identifier_map.get(identifier)
+        if identifier_data is None:
+            logger.warning('Missing data for %s:%s', prefix, identifier)
+            errors.add(identifier)
             continue
 
-        graph[u][v][k][CITATION].update(pmid_data[pmid])
+        graph[u][v][k][CITATION].update(identifier_data)
 
     return errors
+
+
+@lru_cache()
+def get_pmc_csl_item(pmcid: str):
+    """Get the CSL Item for a PubMed Central record by its PMID, PMCID, or DOI, using the NCBI Citation Exporter API."""
+    if not pmcid.startswith("PMC"):
+        raise ValueError(f'not a valid pmd id: {pmcid}')
+
+    from manubot.cite.pubmed import get_pmc_csl_item
+    csl_item = get_pmc_csl_item(pmcid)
+    if "URL" not in csl_item:
+        csl_item["URL"] = f"https://www.ncbi.nlm.nih.gov/pmc/articles/{csl_item.get('PMCID', pmcid)}/"
+    return csl_item
+
+
+def enrich_citation_model_from_pmc(manager, citation, csl) -> bool:
+    """Enrich a citation model with the information from PubMed Central.
+
+    :param pybel.manager.Manager manager:
+    :param Citation citation: A citation model
+    :param dict csl: The dictionary from PMC
+    """
+    citation.title = csl.get('title')
+    citation.journal = csl.get('container-title')
+    citation.volume = csl.get('volume')
+    # citation.issue = csl['issue']
+    citation.pages = csl.get('page')
+    citation.article_type = csl.get('type')
+
+    for author in csl.get('author', []):
+        try:
+            author_name = f'{author["given"]} {author["family"]}'
+        except KeyError:
+            print(f'problem with author in pmc:{citation.db_id}', author)
+            continue
+        author_model = manager.get_or_create_author(author_name)
+        if author_model not in citation.authors:
+            citation.authors.append(author_model)
+
+    if citation.authors:
+        citation.first = citation.authors[0]
+        citation.last = citation.authors[-1]
+
+    issued = csl.get('issued')
+    if issued is not None:
+        date_parts = issued['date-parts'][0]
+        if len(date_parts) == 3:
+            citation.date = date(year=date_parts[0], month=date_parts[1], day=date_parts[2])
+        elif len(date_parts) == 2:
+            citation.date = date(year=date_parts[0], month=date_parts[1], day=1)
+        elif len(date_parts) == 1:
+            citation.date = date(year=date_parts[0], month=1, day=1)
+        else:
+            logger.warning('not sure about date parts: %s', date_parts)
+
+    return True
diff --git a/src/pybel/manager/models.py b/src/pybel/manager/models.py
index d9d4bae9..14f1e332 100644
--- a/src/pybel/manager/models.py
+++ b/src/pybel/manager/models.py
@@ -17,7 +17,7 @@ from ..constants import (
     CITATION, CITATION_AUTHORS, CITATION_DATE, CITATION_FIRST_AUTHOR, CITATION_JOURNAL, CITATION_LAST_AUTHOR, CITATION_PAGES, CITATION_VOLUME, EVIDENCE, IDENTIFIER, METADATA_AUTHORS, METADATA_CONTACT, METADATA_COPYRIGHT, METADATA_DESCRIPTION, METADATA_DISCLAIMER, METADATA_LICENSES, METADATA_NAME, METADATA_VERSION, NAME, NAMESPACE,
 )
 from ..io.gpickle import from_bytes_gz, to_bytes_gz
-from ..language import Entity
+from ..language import CitationDict, Entity
 from ..struct.graph import BELGraph
 from ..tokens import parse_result_to_dsl
 
@@ -439,7 +439,11 @@ class Citation(Base):
         :param bool include_id: If true, includes the model identifier
         :return: Citation dictionary for the recreation of a :class:`BELGraph`.
         """
-        result = Entity(namespace=self.db, identifier=self.db_id)
+        result = CitationDict(
+            namespace=self.db,
+            identifier=self.db_id,
+            name=self.title,
+        )
 
         if include_id:
             result['id'] = self.id
diff --git a/src/pybel/struct/filters/edge_predicates.py b/src/pybel/struct/filters/edge_predicates.py
index b584d4c8..c09281c8 100644
--- a/src/pybel/struct/filters/edge_predicates.py
+++ b/src/pybel/struct/filters/edge_predicates.py
@@ -9,7 +9,7 @@ from .typing import EdgePredicate
 from .utils import part_has_modifier
 from ..graph import BELGraph
 from ...constants import (
-    ACTIVITY, ANNOTATIONS, ASSOCIATION, CAUSAL_RELATIONS, CITATION, CITATION_AUTHORS, CITATION_TYPE_PUBMED, DEGRADATION,
+    ACTIVITY, ANNOTATIONS, ASSOCIATION, CAUSAL_RELATIONS, CITATION, CITATION_AUTHORS, DEGRADATION,
     DIRECT_CAUSAL_RELATIONS, EVIDENCE, NAMESPACE, POLAR_RELATIONS, RELATION, SOURCE_MODIFIER, TARGET_MODIFIER,
     TRANSLOCATION,
 )
@@ -22,6 +22,7 @@ __all__ = [
     'false_edge_predicate',
     'has_provenance',
     'has_pubmed',
+    'has_pmc',
     'has_authors',
     'is_causal_relation',
     'not_causal_relation',
@@ -77,7 +78,19 @@ def has_provenance(edge_data: EdgeData) -> bool:
 @edge_predicate
 def has_pubmed(edge_data: EdgeData) -> bool:
     """Check if the edge has a PubMed citation."""
-    return CITATION in edge_data and edge_data[CITATION][NAMESPACE].lower() in {'pubmed', 'pmid'}
+    return CITATION in edge_data and edge_data[CITATION][NAMESPACE].lower() in ('pubmed', 'pmid')
+
+
+@edge_predicate
+def has_pmc(edge_data: EdgeData) -> bool:
+    """Check if the edge has a PMC citation."""
+    return CITATION in edge_data and edge_data[CITATION][NAMESPACE].lower() in ('pmc', 'pmcid')
+
+
+CITATION_PREDIACATES = {
+    'pubmed': has_pubmed,
+    'pmc': has_pmc,
+}
 
 
 @edge_predicate
diff --git a/src/pybel/struct/summary/provenance.py b/src/pybel/struct/summary/provenance.py
index 084bdadc..3cbe579f 100644
--- a/src/pybel/struct/summary/provenance.py
+++ b/src/pybel/struct/summary/provenance.py
@@ -4,33 +4,77 @@
 
 from typing import Iterable, Set
 
-from ..filters.edge_predicates import has_pubmed
+from ..filters.edge_predicates import CITATION_PREDIACATES
 from ..graph import BELGraph
 from ...constants import CITATION, IDENTIFIER
 
 __all__ = [
     'iterate_pubmed_identifiers',
+    'iterate_pmc_identifiers',
     'get_pubmed_identifiers',
+    'get_pmc_identifiers',
 ]
 
 
-def iterate_pubmed_identifiers(graph: BELGraph) -> Iterable[str]:
-    """Iterate over all PubMed identifiers in a graph.
+def iterate_citation_identifiers(graph, prefix: str):
+    """Iterate over all citation identifiers with the given prefix in a graph.
 
     :param graph: A BEL graph
+    :param prefix: The citation prefix to keep
     :return: An iterator over the PubMed identifiers in the graph
     """
+    predicate = CITATION_PREDIACATES.get(prefix)
+    if predicate is None:
+        raise ValueError(f'Invalid citation prefix: {prefix}')
+
     return (
         data[CITATION][IDENTIFIER].strip()
         for _, _, data in graph.edges(data=True)
-        if has_pubmed(data)
+        if predicate(data)
     )
 
 
+def iterate_pubmed_identifiers(graph: BELGraph) -> Iterable[str]:
+    """Iterate over all PubMed identifiers in a graph.
+
+    :param graph: A BEL graph
+    :return: An iterator over the PubMed identifiers in the graph
+    """
+    return iterate_citation_identifiers(graph, 'pubmed')
+
+
+def iterate_pmc_identifiers(graph: BELGraph) -> Iterable[str]:
+    """Iterate over all PMC identifiers in a graph.
+
+    :param graph: A BEL graph
+    :return: An iterator over the PMC identifiers in the graph
+    """
+    return iterate_citation_identifiers(graph, 'pmc')
+
+
+def get_citation_identifiers(graph: BELGraph, prefix: str) -> Set[str]:
+    """Get the set of all identifiers with the give prefix cited in the construction of a graph.
+
+    :param graph: A BEL graph
+    :param prefix: The citation prefix to keep
+    :return: A set of all PubMed identifiers cited in the construction of this graph
+    """
+    return set(iterate_citation_identifiers(graph, prefix))
+
+
 def get_pubmed_identifiers(graph: BELGraph) -> Set[str]:
     """Get the set of all PubMed identifiers cited in the construction of a graph.
 
     :param graph: A BEL graph
     :return: A set of all PubMed identifiers cited in the construction of this graph
     """
-    return set(iterate_pubmed_identifiers(graph))
+    return get_citation_identifiers(graph, 'pubmed')
+
+
+def get_pmc_identifiers(graph: BELGraph) -> Set[str]:
+    """Get the set of all PMC identifiers cited in the construction of a graph.
+
+    :param graph: A BEL graph
+    :return: A set of all PMC identifiers cited in the construction of this graph
+    """
+    return get_citation_identifiers(graph, 'pmc')


Test patch:
diff --git a/tests/test_manager/pmc_citation_data.json b/tests/test_manager/pmc_citation_data.json
new file mode 100644
index 00000000..539c07a4
--- /dev/null
+++ b/tests/test_manager/pmc_citation_data.json
@@ -0,0 +1,67 @@
+{
+  "PMC6611653": {
+    "source": "PubMed",
+    "accessed": {
+      "date-parts": [
+        [
+          2020,
+          10,
+          9
+        ]
+      ]
+    },
+    "id": "pmid:31233491",
+    "title": "Open collaborative writing with Manubot",
+    "author": [
+      {
+        "family": "Himmelstein",
+        "given": "Daniel S"
+      },
+      {
+        "family": "Rubinetti",
+        "given": "Vincent"
+      },
+      {
+        "family": "Slochower",
+        "given": "David R"
+      },
+      {
+        "family": "Hu",
+        "given": "Dongbo"
+      },
+      {
+        "family": "Malladi",
+        "given": "Venkat S"
+      },
+      {
+        "family": "Greene",
+        "given": "Casey S"
+      },
+      {
+        "family": "Gitter",
+        "given": "Anthony"
+      }
+    ],
+    "container-title-short": "PLoS Comput Biol",
+    "container-title": "PLoS computational biology",
+    "publisher": "Public Library of Science",
+    "ISSN": "1553-734X",
+    "issued": {
+      "date-parts": [
+        [
+          2019,
+          6,
+          24
+        ]
+      ]
+    },
+    "page": "e1007128",
+    "volume": "15",
+    "issue": "6",
+    "PMID": "31233491",
+    "PMCID": "PMC6611653",
+    "DOI": "10.1371/journal.pcbi.1007128",
+    "type": "article-journal",
+    "URL": "https://www.ncbi.nlm.nih.gov/pmc/articles/PMC6611653/"
+  }
+}
\ No newline at end of file
diff --git a/tests/test_manager/citation_data.json b/tests/test_manager/pubmed_citation_data.json
similarity index 100%
rename from tests/test_manager/citation_data.json
rename to tests/test_manager/pubmed_citation_data.json
diff --git a/tests/test_manager/test_citation_utils.py b/tests/test_manager/test_citation_utils.py
index 47cde52e..0b8a08ea 100644
--- a/tests/test_manager/test_citation_utils.py
+++ b/tests/test_manager/test_citation_utils.py
@@ -29,15 +29,23 @@ from pybel.constants import (
     CITATION, CITATION_AUTHORS, CITATION_DATE, CITATION_JOURNAL, CITATION_TYPE_PUBMED,
 )
 from pybel.dsl import Protein
-from pybel.manager.citation_utils import enrich_pubmed_citations, get_citations_by_pmids, sanitize_date
+from pybel.language import CitationDict
+from pybel.manager.citation_utils import (
+    _enrich_citations, enrich_pubmed_citations, get_citations_by_pmids, sanitize_date,
+)
 from pybel.manager.models import Citation
 from pybel.testing.cases import TemporaryCacheMixin
 from pybel.testing.utils import n
 
 HERE = os.path.abspath(os.path.dirname(__file__))
-DATA_PATH = os.path.join(HERE, 'citation_data.json')
-with open(DATA_PATH) as _file:
-    DATA = json.load(_file)
+
+PUBMED_DATA_PATH = os.path.join(HERE, 'pubmed_citation_data.json')
+with open(PUBMED_DATA_PATH) as _file:
+    PUBMED_DATA = json.load(_file)
+
+PMC_DATA_PATH = os.path.join(HERE, 'pmc_citation_data.json')
+with open(PMC_DATA_PATH) as _file:
+    PMC_DATA = json.load(_file)
 
 
 def _mock_fn(pubmed_identifiers: Iterable[str]) -> Mapping[str, Any]:
@@ -45,7 +53,7 @@ def _mock_fn(pubmed_identifiers: Iterable[str]) -> Mapping[str, Any]:
         'uids': pubmed_identifiers,
     }
     for pmid in pubmed_identifiers:
-        result[pmid] = DATA['result'][pmid]
+        result[pmid] = PUBMED_DATA['result'][pmid]
     return {'result': result}
 
 
@@ -55,6 +63,16 @@ mock_get_pubmed_citation_response = mock.patch(
 )
 
 
+def _mock_get_pmc_csl_item(pmc_id: str) -> Mapping[str, Any]:
+    return PMC_DATA[pmc_id]
+
+
+mock_get_pmc_csl_item = mock.patch(
+    'pybel.manager.citation_utils.get_pmc_csl_item',
+    side_effect=_mock_get_pmc_csl_item,
+)
+
+
 class TestSanitizeDate(unittest.TestCase):
     """Test sanitization of dates in various formats."""
 
@@ -91,7 +109,7 @@ class TestSanitizeDate(unittest.TestCase):
         self.assertEqual(None, sanitize_date('2012 Early Spring'))
 
 
-class TestCitations(TemporaryCacheMixin):
+class TestPubmed(TemporaryCacheMixin):
     """Tests for citations."""
 
     def setUp(self):
@@ -102,7 +120,7 @@ class TestCitations(TemporaryCacheMixin):
         self.graph.add_increases(self.u, self.v, citation=self.pmid, evidence=n())
 
     @mock_get_pubmed_citation_response
-    def test_enrich(self, *_):
+    def test_enrich_pubmed(self, *_):
         self.assertEqual(0, self.manager.count_citations())
         get_citations_by_pmids(manager=self.manager, pmids=[self.pmid])
         self.assertEqual(1, self.manager.count_citations())
@@ -114,7 +132,7 @@ class TestCitations(TemporaryCacheMixin):
         self.assertEqual(self.pmid, c.db_id)
 
     @mock_get_pubmed_citation_response
-    def test_enrich_list(self, *_):
+    def test_enrich_pubmed_list(self, *_):
         pmids = [
             '25818332',
             '27003210',
@@ -128,7 +146,7 @@ class TestCitations(TemporaryCacheMixin):
         self.assertIsNotNone(citation)
 
     @mock_get_pubmed_citation_response
-    def test_enrich_list_grouped(self, *_):
+    def test_enrich_pubmed_list_grouped(self, *_):
         pmids = [
             '25818332',
             '27003210',
@@ -142,7 +160,7 @@ class TestCitations(TemporaryCacheMixin):
         self.assertIsNotNone(citation)
 
     @mock_get_pubmed_citation_response
-    def test_enrich_overwrite(self, *_):
+    def test_enrich_pubmed_overwrite(self, *_):
         citation = self.manager.get_or_create_citation(namespace=CITATION_TYPE_PUBMED, identifier=self.pmid)
         self.manager.session.commit()
         self.assertIsNone(citation.date)
@@ -152,6 +170,7 @@ class TestCitations(TemporaryCacheMixin):
 
         _, _, d = list(self.graph.edges(data=True))[0]
         citation_dict = d[CITATION]
+        self.assertIsInstance(citation_dict, CitationDict)
 
         self.assertIn(CITATION_JOURNAL, citation_dict)
         self.assertIn(CITATION_DATE, citation_dict)
@@ -164,11 +183,12 @@ class TestCitations(TemporaryCacheMixin):
         )
 
     @mock_get_pubmed_citation_response
-    def test_enrich_graph(self, *_):
+    def test_enrich_pubmed_graph(self, *_):
         enrich_pubmed_citations(manager=self.manager, graph=self.graph)
 
         _, _, d = list(self.graph.edges(data=True))[0]
         citation_dict = d[CITATION]
+        self.assertIsInstance(citation_dict, CitationDict)
 
         self.assertIn(CITATION_JOURNAL, citation_dict)
 
@@ -183,7 +203,7 @@ class TestCitations(TemporaryCacheMixin):
 
     @mock_get_pubmed_citation_response
     @unittest.skipIf(os.environ.get('DB') == 'mysql', reason='MySQL collation is wonky')
-    def test_accent_duplicate(self, *_):
+    def test_enrich_pubmed_accent_duplicate(self, *_):
         """Test when two authors, Gomez C and Goméz C are both checked that they are not counted as duplicates."""
         g1 = 'Gomez C'
         g2 = 'Gómez C'
@@ -207,3 +227,34 @@ class TestCitations(TemporaryCacheMixin):
 
         a2 = self.manager.get_author_by_name(g2)
         self.assertEqual(g2, a2.name)
+
+
+class TestPMC(TemporaryCacheMixin):
+    """Tests for citations."""
+
+    def setUp(self):
+        super().setUp()
+        self.u, self.v = (Protein(n(), n()) for _ in range(2))
+        self.citation_identifier = 'PMC6611653'
+        self.graph = BELGraph()
+        self.graph.add_increases(self.u, self.v, citation=('pmc', self.citation_identifier), evidence=n())
+
+    @mock_get_pmc_csl_item
+    def test_enrich_pmc(self, *_):
+        errors = _enrich_citations(manager=self.manager, graph=self.graph, prefix='pmc')
+        self.assertEqual(0, len(errors), msg=f'Got errors: {errors}')
+        _, _, d = list(self.graph.edges(data=True))[0]
+        citation_dict = d[CITATION]
+        self.assertIsInstance(citation_dict, CitationDict)
+        self.assertEqual('pmc', citation_dict.namespace)
+        self.assertEqual(self.citation_identifier, citation_dict.identifier)
+
+        self.assertIn(CITATION_JOURNAL, citation_dict)
+        self.assertEqual('PLoS computational biology', citation_dict[CITATION_JOURNAL])
+
+        self.assertIn(CITATION_DATE, citation_dict)
+        self.assertEqual('2019-06-24', citation_dict[CITATION_DATE])
+
+        self.assertIn(CITATION_AUTHORS, citation_dict)
+        self.assertLess(0, len(citation_dict[CITATION_AUTHORS]))
+        # TODO the eUtils and CSL thing both normalize the way autors look