Repository: pybel/pybel
Pull request #468 (2020-09-25 09:31:23)

Update citations

codecov[bot]: # [Codecov](https://codecov.io/gh/pybel/pybel/pull/468?src=pr&el=h1) Report
> Merging [#468](https://codecov.io/gh/pybel/pybel/pull/468?src=pr&el=desc) into [master](https://codecov.io/gh/pybel/pybel/commit/4ace8ae3501b008952c0f4a3749e511c7b729f5e?el=desc) will **increase** coverage by `0.01%`.
> The diff coverage is `85.24%`.

[![Impacted file tree graph](https://codecov.io/gh/pybel/pybel/pull/468/graphs/tree.svg?width=650&height=150&src=pr&token=J7joRTRygG)](https://codecov.io/gh/pybel/pybel/pull/468?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #468      +/-   ##
==========================================
+ Coverage   78.28%   78.30%   +0.01%     
==========================================
  Files         176      176              
  Lines        9530     9546      +16     
  Branches     1402     1406       +4     
==========================================
+ Hits         7461     7475      +14     
- Misses       1768     1770       +2     
  Partials      301      301              
```


| [Impacted Files](https://codecov.io/gh/pybel/pybel/pull/468?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/pybel/manager/citation\_utils.py](https://codecov.io/gh/pybel/pybel/pull/468/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL21hbmFnZXIvY2l0YXRpb25fdXRpbHMucHk=) | `83.33% <75.67%> (-2.27%)` | :arrow_down: |
| [src/pybel/constants.py](https://codecov.io/gh/pybel/pybel/pull/468/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL2NvbnN0YW50cy5weQ==) | `100.00% <100.00%> (ø)` | |
| [src/pybel/io/jgif.py](https://codecov.io/gh/pybel/pybel/pull/468/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL2lvL2pnaWYucHk=) | `72.09% <100.00%> (+0.49%)` | :arrow_up: |
| [src/pybel/manager/models.py](https://codecov.io/gh/pybel/pybel/pull/468/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL21hbmFnZXIvbW9kZWxzLnB5) | `88.76% <100.00%> (+0.17%)` | :arrow_up: |
| [src/pybel/parser/parse\_control.py](https://codecov.io/gh/pybel/pybel/pull/468/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL3BhcnNlci9wYXJzZV9jb250cm9sLnB5) | `100.00% <100.00%> (ø)` | |
| [src/pybel/manager/cache\_manager.py](https://codecov.io/gh/pybel/pybel/pull/468/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL21hbmFnZXIvY2FjaGVfbWFuYWdlci5weQ==) | `80.73% <0.00%> (+0.40%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pybel/pybel/pull/468?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pybel/pybel/pull/468?src=pr&el=footer). Last update [4ace8ae...005bf62](https://codecov.io/gh/pybel/pybel/pull/468?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/setup.cfg b/setup.cfg
index b0de7ca0..881190db 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -66,6 +66,7 @@ install_requires =
     tabulate
     pandas
     jsonschema
+    ratelimit
 
 # Random options
 zip_safe = false
diff --git a/src/pybel/constants.py b/src/pybel/constants.py
index dca4afe1..e13dd2f3 100644
--- a/src/pybel/constants.py
+++ b/src/pybel/constants.py
@@ -23,25 +23,24 @@ def get_cache_connection() -> str:
 PYBEL_CONTEXT_TAG = 'pybel_context'
 PYBEL_AUTOEVIDENCE = 'Automatically added by PyBEL'
 
-CITATION_TYPE_BOOK = 'Book'
-CITATION_TYPE_PUBMED = 'PubMed'
-CITATION_TYPE_PMC = 'PubMed Central'
-CITATION_TYPE_JOURNAL = 'Journal'
-CITATION_TYPE_ONLINE = 'Online Resource'
-CITATION_TYPE_URL = 'URL'
-CITATION_TYPE_DOI = 'DOI'
-CITATION_TYPE_OTHER = 'Other'
-#: The valid citation types
-#: .. seealso:: https://wiki.openbel.org/display/BELNA/Citation
+CITATION_TYPE_BOOK = 'book'
+CITATION_TYPE_PUBMED = 'pubmed'
+CITATION_TYPE_PMC = 'pmc'
+CITATION_TYPE_URL = 'url'
+CITATION_TYPE_DOI = 'doi'
+CITATION_TYPE_OTHER = 'other'
 CITATION_TYPES = {
-    CITATION_TYPE_BOOK: None,
-    CITATION_TYPE_PUBMED: 'pmid',
-    CITATION_TYPE_PMC: 'pmc',
-    CITATION_TYPE_JOURNAL: None,
-    CITATION_TYPE_ONLINE: None,
-    CITATION_TYPE_URL: None,
-    CITATION_TYPE_DOI: 'doi',
-    CITATION_TYPE_OTHER: None,
+    CITATION_TYPE_BOOK,
+    CITATION_TYPE_PUBMED,
+    CITATION_TYPE_PMC,
+    CITATION_TYPE_URL,
+    CITATION_TYPE_DOI,
+    CITATION_TYPE_OTHER,
+}
+CITATION_NORMALIZER = {
+    'pubmed central': 'pmc',
+    'pmid': 'pubmed',
+    'online resource': 'url',
 }
 
 NAMESPACE_DOMAIN_BIOPROCESS = 'BiologicalProcess'
@@ -73,6 +72,8 @@ CITATION_PAGES = 'pages'
 CITATION_FIRST_AUTHOR = 'first'
 #: Represents the key for the optional PyBEL citation last author entry in a citation dictionary
 CITATION_LAST_AUTHOR = 'last'
+#: Represents the type of article (Journal Article, Review, etc.)
+CITATION_ARTICLE_TYPE = 'article_type'
 
 # Used during BEL parsing
 
diff --git a/src/pybel/io/jgif.py b/src/pybel/io/jgif.py
index 443d0ce1..4566b103 100644
--- a/src/pybel/io/jgif.py
+++ b/src/pybel/io/jgif.py
@@ -19,6 +19,7 @@ import requests
 from networkx.utils import open_file
 from pyparsing import ParseException
 
+from .. import constants as pc
 from ..constants import (
     ANNOTATIONS, CITATION, EVIDENCE, GRAPH_ANNOTATION_URL, GRAPH_NAMESPACE_URL, METADATA_AUTHORS, METADATA_CONTACT,
     METADATA_INSERT_KEYS, METADATA_LICENSES, RELATION, UNQUALIFIED_EDGES,
@@ -309,7 +310,9 @@ def from_jgif(graph_jgif_dict, parser_kwargs: Optional[Mapping[str, Any]] = None
                     continue
 
                 parser.control_parser.clear()
-                parser.control_parser.citation_db = citation['type'].strip()
+                citation_namespace = citation['type'].lower().strip()
+                citation_namespace = pc.CITATION_NORMALIZER.get(citation_namespace, citation_namespace)
+                parser.control_parser.citation_db = citation_namespace
                 parser.control_parser.citation_db_id = citation['id'].strip()
                 parser.control_parser.evidence = summary_text
                 annotations = parser.graph._clean_annotations(evidence[EXPERIMENT_CONTEXT])
diff --git a/src/pybel/manager/citation_utils.py b/src/pybel/manager/citation_utils.py
index 11de63fc..7ae44ec7 100644
--- a/src/pybel/manager/citation_utils.py
+++ b/src/pybel/manager/citation_utils.py
@@ -6,12 +6,16 @@ import logging
 import re
 import time
 from datetime import datetime
-from itertools import zip_longest
 from typing import Dict, Iterable, List, Optional, Set, Tuple, Union
 
+import ratelimit
 import requests
+from more_itertools import chunked
+from sqlalchemy import and_
+from tqdm import tqdm
 
-from ..constants import CITATION, CITATION_TYPE_PUBMED, IDENTIFIER
+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
@@ -33,6 +37,9 @@ re5 = re.compile(r'^([12][0-9]{3}) (Spring|Fall|Winter|Summer)$')
 re6 = re.compile(r'^[12][0-9]{3} [a-zA-Z]{3} \d{1,2}-(\d{1,2})$')
 re7 = re.compile(r'^[12][0-9]{3} [a-zA-Z]{3} \d{1,2}-([a-zA-Z]{3} \d{1,2})$')
 
+# TODO "Winter 2016" probably with re.compile(r'^(Spring|Fall|Winter|Summer) ([12][0-9]{3})$')
+# TODO "YYYY Oct - Dec" update re4 to allow spaces before and after the dash
+
 season_map = {'Spring': '03', 'Summer': '06', 'Fall': '09', 'Winter': '12'}
 
 
@@ -67,23 +74,18 @@ def sanitize_date(publication_date: str) -> str:
         return datetime.strptime(publication_date, '%Y %b %d-{}'.format(s.groups()[0])).strftime('%Y-%m-%d')
 
 
-def grouper(n, iterable, fillvalue=None):
-    """Group iterables into tuples.
-
-    grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx
-    """
-    args = [iter(iterable)] * n
-    return zip_longest(*args, fillvalue=fillvalue)
-
-
 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})
 
 
+@ratelimit.limits(calls=3, period=1)
 def get_pubmed_citation_response(pubmed_identifiers: Iterable[str]):
     """Get the response from PubMed E-Utils for a given list of PubMed identifiers.
 
+    Rate limit of 3 requests per second is from:
+    https://ncbiinsights.ncbi.nlm.nih.gov/2018/08/14/release-plan-for-e-utility-api-keys/
+
     :param pubmed_identifiers:
     :rtype: dict
     """
@@ -117,6 +119,9 @@ def enrich_citation_model(manager, citation, p) -> bool:
     citation.pages = p['pages']
     citation.first = manager.get_or_create_author(p['sortfirstauthor'])
     citation.last = manager.get_or_create_author(p['lastauthor'])
+    pubtypes = p['pubtype']
+    if pubtypes:
+        citation.article_type = pubtypes[0]
 
     if 'authors' in p:
         for author in p['authors']:
@@ -125,7 +130,12 @@ def enrich_citation_model(manager, citation, p) -> bool:
                 citation.authors.append(author_model)
 
     publication_date = p['pubdate']
-    sanitized_publication_date = sanitize_date(publication_date)
+    try:
+        sanitized_publication_date = sanitize_date(publication_date)
+    except ValueError:
+        logger.warning('could not parse publication date %s for pubmed:%s', publication_date, citation.db_id)
+        sanitized_publication_date = None
+
     if sanitized_publication_date:
         citation.date = datetime.strptime(sanitized_publication_date, '%Y-%m-%d')
     else:
@@ -137,53 +147,54 @@ def enrich_citation_model(manager, citation, p) -> bool:
 def get_citations_by_pmids(
     manager,
     pmids: Iterable[Union[str, int]],
+    *,
     group_size: Optional[int] = None,
-    sleep_time: Optional[int] = None,
+    offline: bool = False,
 ) -> 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 group_size: The number of PubMed identifiers to query at a time. Defaults to 200 identifiers.
-    :param sleep_time: Number of seconds to sleep between queries. Defaults to 1 second.
     :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`
     """
     group_size = group_size if group_size is not None else 200
-    sleep_time = sleep_time if sleep_time is not None else 1
 
     pmids = clean_pubmed_identifiers(pmids)
-    logger.info('Ensuring %d PubMed identifiers', len(pmids))
+    logger.info('ensuring %d PubMed identifiers', len(pmids))
 
-    result = {}
+    enriched_pmids = {}
     unenriched_pmids = {}
 
-    for pmid in pmids:
-        citation = manager.get_or_create_citation(namespace=CITATION_TYPE_PUBMED, identifier=pmid)
-
-        if not citation.is_enriched:
+    citation_filter = and_(
+        models.Citation.db == 'pubmed',
+        models.Citation.db_id.in_(pmids),
+    )
+    citation_models = manager.session.query(models.Citation).filter(citation_filter).all()
+    pmid_to_model = {
+        citation_model.db_id: citation_model
+        for citation_model in citation_models
+    }
+    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()
+        else:
             unenriched_pmids[pmid] = citation
-            continue
-
-        result[pmid] = citation.to_json()
 
+    logger.info('%d of %d are already enriched', len(enriched_pmids), len(pmids))
     manager.session.commit()
 
-    logger.debug('Found %d PubMed identifiers in database', len(pmids) - len(unenriched_pmids))
-
-    if not unenriched_pmids:
-        return result, set()
-
-    number_unenriched = len(unenriched_pmids)
-    logger.info('Querying PubMed for %d identifiers', number_unenriched)
-
     errors = set()
-    t = time.time()
-
-    for pmid_group_index, pmid_list in enumerate(grouper(group_size, unenriched_pmids), start=1):
-        pmid_list = list(pmid_list)
-        logger.info('Getting group %d having %d PubMed identifiers', pmid_group_index, len(pmid_list))
+    if not unenriched_pmids or offline:
+        return enriched_pmids, errors
 
+    it = tqdm(unenriched_pmids, desc=f'getting PubMed data in chunks of {group_size}')
+    for pmid_list in chunked(it, n=group_size):
         response = get_pubmed_citation_response(pmid_list)
         response_pmids = response['result']['uids']
 
@@ -194,28 +205,23 @@ def get_citations_by_pmids(
             successful_enrichment = enrich_citation_model(manager, citation, p)
 
             if not successful_enrichment:
-                logger.warning("Error downloading PubMed identifier: %s", pmid)
+                it.write(f"Error downloading PubMed identifier: {pmid}")
                 errors.add(pmid)
                 continue
 
-            result[pmid] = citation.to_json()
+            enriched_pmids[pmid] = citation.to_json()
             manager.session.add(citation)
 
         manager.session.commit()  # commit in groups
 
-        # Don't want to hit that rate limit
-        time.sleep(sleep_time)
-
-    logger.info('retrieved %d PubMed identifiers in %.02f seconds', len(unenriched_pmids), time.time() - t)
-
-    return result, errors
+    return enriched_pmids, errors
 
 
 def enrich_pubmed_citations(
     manager,
     graph,
     group_size: Optional[int] = None,
-    sleep_time: Optional[int] = None,
+    offline: bool = False,
 ) -> Set[str]:
     """Overwrite all PubMed citations with values from NCBI's eUtils lookup service.
 
@@ -225,14 +231,13 @@ def enrich_pubmed_citations(
     :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 sleep_time: Number of seconds to sleep between queries. Defaults to 1 second.
     :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, sleep_time=sleep_time)
+    pmid_data, errors = get_citations_by_pmids(manager, pmids=pmids, group_size=group_size, offline=offline)
 
     for u, v, k in filter_edges(graph, has_pubmed):
-        pmid = graph[u][v][k][CITATION][IDENTIFIER].strip()
+        pmid = graph[u][v][k][CITATION].identifier
 
         if pmid not in pmid_data:
             logger.warning('Missing data for PubMed identifier: %s', pmid)
diff --git a/src/pybel/manager/models.py b/src/pybel/manager/models.py
index 2cd58613..8f767dbb 100644
--- a/src/pybel/manager/models.py
+++ b/src/pybel/manager/models.py
@@ -12,11 +12,9 @@ from sqlalchemy import (
 from sqlalchemy.ext.declarative import declarative_base
 from sqlalchemy.orm import backref, relationship
 
+from .. import constants as pc
 from ..constants import (
-    CITATION, CITATION_AUTHORS, CITATION_DATE, CITATION_FIRST_AUTHOR, CITATION_JOURNAL, CITATION_LAST_AUTHOR,
-    CITATION_PAGES, CITATION_TYPE_PUBMED, CITATION_VOLUME, EVIDENCE, IDENTIFIER, METADATA_AUTHORS, METADATA_CONTACT,
-    METADATA_COPYRIGHT, METADATA_DESCRIPTION, METADATA_DISCLAIMER, METADATA_LICENSES, METADATA_NAME, METADATA_VERSION,
-    NAME, NAMESPACE,
+    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
@@ -399,6 +397,7 @@ class Citation(Base):
     db = Column(String(16), nullable=False, doc='Type of the stored publication e.g. PubMed')
     db_id = Column(String(255), nullable=False, doc='Reference identifier of the publication e.g. PubMed_ID')
 
+    article_type = Column(Text, nullable=True, doc='Type of the publication')
     title = Column(Text, nullable=True, doc='Title of the publication')
     journal = Column(Text, nullable=True, doc='Journal name')
     volume = Column(Text, nullable=True, doc='Volume of the journal')
@@ -424,7 +423,7 @@ class Citation(Base):
     @property
     def is_pubmed(self) -> bool:
         """Return if this is a PubMed citation."""
-        return CITATION_TYPE_PUBMED == self.db
+        return self.db == 'pubmed'
 
     @property
     def is_enriched(self) -> bool:
@@ -463,6 +462,9 @@ class Citation(Base):
         if self.last:
             result[CITATION_LAST_AUTHOR] = self.last.name
 
+        if self.article_type:
+            result[pc.CITATION_ARTICLE_TYPE] = self.article_type
+
         if self.authors:
             result[CITATION_AUTHORS] = sorted(
                 author.name
diff --git a/src/pybel/parser/parse_control.py b/src/pybel/parser/parse_control.py
index 0a489632..7ba48a4a 100644
--- a/src/pybel/parser/parse_control.py
+++ b/src/pybel/parser/parse_control.py
@@ -16,9 +16,10 @@ from pyparsing import And, Keyword, MatchFirst, ParseResults, Suppress, oneOf, p
 
 from .baseparser import BaseParser
 from .utils import delimited_quoted_list, delimited_unquoted_list, is_int, qid, quote
+from .. import constants as pc
 from ..constants import (
     ANNOTATIONS, BEL_KEYWORD_ALL, BEL_KEYWORD_CITATION, BEL_KEYWORD_EVIDENCE, BEL_KEYWORD_SET,
-    BEL_KEYWORD_STATEMENT_GROUP, BEL_KEYWORD_SUPPORT, BEL_KEYWORD_UNSET, CITATION, CITATION_TYPES, CITATION_TYPE_PUBMED,
+    BEL_KEYWORD_STATEMENT_GROUP, BEL_KEYWORD_SUPPORT, BEL_KEYWORD_UNSET, CITATION, CITATION_TYPES,
     EVIDENCE,
 )
 from ..exceptions import (
@@ -221,10 +222,10 @@ class ControlParser(BaseParser):
         if len(values) < 2:
             raise CitationTooShortException(self.get_line_number(), line, position)
 
-        citation_db = values[0]
-
-        if citation_db not in CITATION_TYPES:
-            raise InvalidCitationType(self.get_line_number(), line, position, citation_db)
+        citation_namespace = values[0].lower()
+        citation_namespace = pc.CITATION_NORMALIZER.get(citation_namespace, citation_namespace)
+        if citation_namespace not in CITATION_TYPES:
+            raise InvalidCitationType(self.get_line_number(), line, position, citation_namespace)
 
         if 2 == len(values):
             citation_db_id = values[1]
@@ -240,10 +241,10 @@ class ControlParser(BaseParser):
 
             citation_db_id = values[2]
 
-        if citation_db == CITATION_TYPE_PUBMED and not is_int(citation_db_id):
+        if citation_namespace == 'pubmed' and not is_int(citation_db_id):
             raise InvalidPubMedIdentifierWarning(self.get_line_number(), line, position, citation_db_id)
 
-        self.citation_db = citation_db
+        self.citation_db = citation_namespace
         self.citation_db_id = citation_db_id
         return tokens
 


Test patch:
diff --git a/src/pybel/testing/resources/bel/slushy.bel b/src/pybel/testing/resources/bel/slushy.bel
index ab8822fd..7eba602c 100644
--- a/src/pybel/testing/resources/bel/slushy.bel
+++ b/src/pybel/testing/resources/bel/slushy.bel
@@ -34,7 +34,7 @@ SET STATEMENT_GROUP = "Group 1"
 SET Citation = {"PubMed"}
 
 # InvalidCitationType
-SET Citation = {"Pubmed", "Incomplete", "1234"}
+SET Citation = {"Nope", "Incomplete", "1234"}
 
 # InvalidPubMedIdentifierWarning
 SET Citation = {"PubMed", "Fake Name", "Fake Reference"}
@@ -106,3 +106,6 @@ SET STATEMENT_GROUP = "Group 2"
 SET Citation = {"PubMed","That one article from last week","123455"}
 SET Evidence = "Evidence 1"
 p(HGNC:AKT1) -> p(HGNC:EGFR)
+
+# As of PyBEL 15, the type is auto-lowercased, so this is valid now
+SET Citation = {"Pubmed", "Incomplete", "1234"}
diff --git a/tests/constants.py b/tests/constants.py
index d98d6759..67cdd410 100644
--- a/tests/constants.py
+++ b/tests/constants.py
@@ -17,7 +17,8 @@ from pybel.constants import (
 from pybel.dsl import BaseEntity, ComplexAbundance, Pathology, Protein
 from pybel.dsl.namespaces import hgnc
 from pybel.exceptions import (
-    BELParserWarning, BELSyntaxError, IllegalAnnotationValueWarning, InvalidCitationLengthException,
+    BELParserWarning, BELSyntaxError, CitationTooShortException, IllegalAnnotationValueWarning,
+    InvalidCitationLengthException,
     InvalidCitationType, InvalidFunctionSemantic, InvalidPubMedIdentifierWarning, MalformedTranslocationWarning,
     MissingAnnotationKeyWarning, MissingAnnotationRegexWarning, MissingCitationException, MissingMetadataException,
     MissingNamespaceNameWarning, MissingNamespaceRegexWarning, MissingSupportWarning, NakedNameWarning,
@@ -410,7 +411,7 @@ class BelReconstitutionMixin(TestGraphMixin):
                 (3, VersionFormatWarning),
                 (26, MissingAnnotationKeyWarning),
                 (29, MissingAnnotationKeyWarning),
-                (34, InvalidCitationLengthException),
+                (34, CitationTooShortException),
                 (37, InvalidCitationType),
                 (40, InvalidPubMedIdentifierWarning),
                 (43, MissingCitationException),
@@ -436,6 +437,12 @@ class BelReconstitutionMixin(TestGraphMixin):
             for warning_tuple in graph.warnings:
                 self.assertEqual(3, len(warning_tuple), msg='Warning tuple is wrong size: {}'.format(warning_tuple))
 
+            _sliced_warnings = [
+                (w.line_number, w.__class__)
+                for _, w, _ in graph.warnings
+            ]
+            self.assertEqual(expected_warnings, _sliced_warnings, msg='wrong warnings')
+
             for (el, ew), (_, exc, _) in zip(expected_warnings, graph.warnings):
                 self.assertIsInstance(exc, BELParserWarning)
                 self.assertIsInstance(exc.line, str)
diff --git a/tests/test_canonicalization.py b/tests/test_canonicalization.py
index 3980a8b9..ec927d9d 100644
--- a/tests/test_canonicalization.py
+++ b/tests/test_canonicalization.py
@@ -7,7 +7,7 @@ from typing import Iterable
 
 from pybel import BELGraph
 from pybel.canonicalize import _to_bel_lines_body, postpend_location
-from pybel.constants import EXTRACELLULAR, INTRACELLULAR, MODIFIER
+from pybel.constants import EXTRACELLULAR, INTRACELLULAR, MODIFIER, CITATION_TYPE_PUBMED
 from pybel.dsl import (
     Abundance, BiologicalProcess, ComplexAbundance, CompositeAbundance, EnumeratedFusionRange, Fragment, Gene,
     GeneFusion, GeneModification, Hgvs, MicroRna, NamedComplexAbundance, Pathology, Protein, ProteinModification,
@@ -258,7 +258,7 @@ class TestSerializeBEL(unittest.TestCase):
         self.assertEqual(1, self.graph.number_of_edges())
 
         expected_lines = [
-            'SET Citation = {{"PubMed", "{}"}}\n'.format(self.citation),
+            f'SET Citation = {{"{CITATION_TYPE_PUBMED}", "{self.citation}"}}\n',
             'SET SupportingText = "{}"'.format(self.evidence),
             'p(HGNC:YFG1) increases p(HGNC:YFG)',
             'UNSET SupportingText',
@@ -286,7 +286,7 @@ class TestSerializeBEL(unittest.TestCase):
         self.assertEqual(1, self.graph.number_of_edges())
 
         expected_lines = [
-            f'SET Citation = {{"PubMed", "{self.citation}"}}\n',
+            f'SET Citation = {{"{CITATION_TYPE_PUBMED}", "{self.citation}"}}\n',
             f'SET SupportingText = "{self.evidence}"',
             f'SET {key} = "{namespace}:{value}"',
             'p(HGNC:YFG1) increases p(HGNC:YFG)',
@@ -319,7 +319,7 @@ class TestSerializeBEL(unittest.TestCase):
         self.assertNotIn(a1, self.graph.annotation_curie)
 
         expected_lines = [
-            'SET Citation = {{"PubMed", "{}"}}\n'.format(self.citation),
+            f'SET Citation = {{"{CITATION_TYPE_PUBMED}", "{self.citation}"}}\n',
             f'SET SupportingText = "{self.evidence}"',
             f'SET {a1} = "{v1}"',
             'p(HGNC:YFG1) increases p(HGNC:YFG)',
@@ -350,7 +350,7 @@ class TestSerializeBEL(unittest.TestCase):
         self.assertEqual(1, self.graph.number_of_edges())
 
         expected_lines = [
-            f'SET Citation = {{"PubMed", "{self.citation}"}}\n',
+            f'SET Citation = {{"{CITATION_TYPE_PUBMED}", "{self.citation}"}}\n',
             f'SET SupportingText = "{self.evidence}"',
             f'SET {a1} = {{"{v1}", "{v2}"}}',
             'p(HGNC:YFG1) increases p(HGNC:YFG)',
diff --git a/tests/test_io/test_import.py b/tests/test_io/test_import.py
index cd16aa43..fb53d62e 100644
--- a/tests/test_io/test_import.py
+++ b/tests/test_io/test_import.py
@@ -247,7 +247,7 @@ class TestInterchange(TemporaryCacheClsMixin, BelReconstitutionMixin):
     def test_slushy_json(self):
         graph_json = to_nodelink(self.slushy_graph)
         graph = from_nodelink(graph_json)
-        self.bel_slushy_reconstituted(graph)
+        self.bel_slushy_reconstituted(graph, check_warnings=False)
 
     def test_slushy_graphml(self):
         handle, path = tempfile.mkstemp()
diff --git a/tests/test_struct/test_filters/test_node_predicates.py b/tests/test_struct/test_filters/test_node_predicates.py
index dfadb4e0..5b0e9927 100644
--- a/tests/test_struct/test_filters/test_node_predicates.py
+++ b/tests/test_struct/test_filters/test_node_predicates.py
@@ -6,10 +6,10 @@ import unittest
 
 from pybel import BELGraph
 from pybel.constants import (
-    ACTIVITY, ANNOTATIONS, ASSOCIATION, CAUSES_NO_CHANGE, CITATION, CITATION_AUTHORS, CITATION_TYPE_ONLINE,
-    CITATION_TYPE_PUBMED, DECREASES, DEGRADATION, DIRECTLY_DECREASES, DIRECTLY_INCREASES, EVIDENCE, GMOD, IDENTIFIER,
-    INCREASES, LOCATION, MODIFIER, NAMESPACE, TARGET_MODIFIER, POLAR_RELATIONS, POSITIVE_CORRELATION, RELATION, SOURCE_MODIFIER,
-    TRANSLOCATION,
+    ACTIVITY, ANNOTATIONS, ASSOCIATION, CAUSES_NO_CHANGE, CITATION, CITATION_AUTHORS, CITATION_TYPE_PUBMED,
+    CITATION_TYPE_URL, DECREASES, DEGRADATION, DIRECTLY_DECREASES, DIRECTLY_INCREASES, EVIDENCE, GMOD, IDENTIFIER,
+    INCREASES, LOCATION, MODIFIER, NAMESPACE, POLAR_RELATIONS, POSITIVE_CORRELATION, RELATION, SOURCE_MODIFIER,
+    TARGET_MODIFIER, TRANSLOCATION,
 )
 from pybel.dsl import (
     abundance, activity, degradation, fragment, gene, gmod, hgvs, pmod, protein, secretion, translocation,
@@ -150,7 +150,7 @@ class TestNodePredicates(unittest.TestCase):
             u,
             v,
             citation={
-                NAMESPACE: CITATION_TYPE_ONLINE, IDENTIFIER: 'https://www.ncbi.nlm.nih.gov/gene/3290'
+                NAMESPACE: CITATION_TYPE_URL, IDENTIFIER: 'https://www.ncbi.nlm.nih.gov/gene/3290'
             },
             evidence="Entrez Gene Summary: Human: The protein encoded by this gene is a microsomal enzyme that "
                      "catalyzes the conversion of the stress hormone cortisol to the inactive metabolite cortisone. "
@@ -426,7 +426,7 @@ class TestEdgePredicate(unittest.TestCase):
 
     def test_has_pubmed(self):
         self.assertTrue(has_pubmed({CITATION: {NAMESPACE: CITATION_TYPE_PUBMED}}))
-        self.assertFalse(has_pubmed({CITATION: {NAMESPACE: CITATION_TYPE_ONLINE}}))
+        self.assertFalse(has_pubmed({CITATION: {NAMESPACE: CITATION_TYPE_URL}}))
         self.assertFalse(has_pubmed({}))
 
     def test_has_authors(self):