Repository: biocommons/bioutils

Issue:
Add support for eutils' api_key parameter
Utils will require an api_key starting May 1, 2018.
We should support adding an api_key as an environment variable.

Discussion:

Patch:
diff --git a/.gitignore b/.gitignore
index b6e83da..c1fee79 100644
--- a/.gitignore
+++ b/.gitignore
@@ -94,5 +94,3 @@ ENV/
 doc/_build
 .eggs
 bioutils/_data/assemblies/pull
-.pytest_cache
-.vscode
diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index b933f71..0000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-    "python.formatting.provider": "yapf",
-    "editor.formatOnSave": true,
-    "python.venvPath": "${workspaceFolder}/venv/",
-    "python.pythonPath": "${workspaceFolder}/venv/3.6/bin/python",
-}
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 5ce8a7b..367f174 100644
--- a/Makefile
+++ b/Makefile
@@ -10,7 +10,7 @@ SELF:=$(firstword $(MAKEFILE_LIST))
 PKG=bioutils
 PKGD=$(subst .,/,${PKG})
 
-VEDIR=venv/3.6
+VEDIR=venv/3.5
 
 
 ############################################################################
diff --git a/README.rst b/README.rst
index a126048..224f8e9 100644
--- a/README.rst
+++ b/README.rst
@@ -9,6 +9,8 @@ the `hgvs <https://github.com/biocommons/hgvs/>`_ and `uta
 not really intended for broader use (read: it may change without
 notice).
 
+To use an E-Utilities API key run add it to an environment variable called `ncbi_api_key`
+and it will be used in the E-Utilities request.
 
 .. |issues_badge| image:: https://img.shields.io/github/issues/biocommons/bioutils.png
   :target: https://github.com/biocommons/bioutils/issues
diff --git a/bioutils/accessions.py b/bioutils/accessions.py
index c80a428..c10a9b5 100644
--- a/bioutils/accessions.py
+++ b/bioutils/accessions.py
@@ -5,154 +5,8 @@
 
 from __future__ import absolute_import, division, print_function, unicode_literals
 
-import re
-
 from six import iteritems
 
-from .exceptions import BioutilsError
-
-
-_ensembl_species_prefixes = "|".join("""ENS ENSACA ENSAME ENSAMX
-ENSANA ENSAPL ENSBTA ENSCAF ENSCAN ENSCAP ENSCAT ENSCCA ENSCEL ENSCGR
-ENSCGR ENSCHI ENSCHO ENSCIN ENSCJA ENSCLA ENSCPO ENSCSA ENSCSAV ENSDAR
-ENSDNO ENSDOR ENSEBU ENSECA ENSEEU ENSETE ENSFAL ENSFCA ENSFDA ENSGAC
-ENSGAL ENSGGO ENSGMO ENSHGLF ENSHGLM ENSJJA ENSLAC ENSLAF ENSLOC
-ENSMAU ENSMEU ENSMFA ENSMGA ENSMIC ENSMLE ENSMLU ENSMMU ENSMNE ENSMOC
-ENSMOD ENSMPU ENSMUS ENSNGA ENSNLE ENSOAN ENSOAR ENSOCU ENSODE ENSOGA
-ENSONI ENSOPR ENSORL ENSPAN ENSPCA ENSPCO ENSPEM ENSPFO ENSPMA ENSPPA
-ENSPPR ENSPPY ENSPSI ENSPTI ENSPTR ENSPVA ENSRBI ENSRNO ENSRRO ENSSAR
-ENSSBO ENSSCE ENSSHA ENSSSC ENSSTO ENSTBE ENSTGU ENSTNI ENSTRU ENSTSY
-ENSTTR ENSVPA ENSXET ENSXMA FB MGP_129S1SvImJ_ MGP_AJ_ MGP_AKRJ_
-MGP_BALBcJ_ MGP_C3HHeJ_ MGP_C57BL6NJ_ MGP_CAROLIEiJ_ MGP_CASTEiJ_
-MGP_CBAJ_ MGP_DBA2J_ MGP_FVBNJ_ MGP_LPJ_ MGP_NODShiLtJ_ MGP_NZOHlLtJ_
-MGP_PWKPhJ_ MGP_PahariEiJ_ MGP_SPRETEiJ_ MGP_WSBEiJ_""".split())
-_ensembl_feature_types_re = r"E|FM|G|GT|P|R|T"
-_ensembl_re = r"^(?:{})(?:{}){}$".format(
-    _ensembl_species_prefixes, _ensembl_feature_types_re, r"\d{11}(?:\.\d+)?")
-
-ac_namespace_regexps = {
-    # https://uswest.ensembl.org/info/genome/stable_ids/prefixes.html
-    # [species prefix][feature type prefix][a unique eleven digit number]
-    # N.B. The regexp at http://identifiers.org/ensembl appears broken:
-    # 1) Human only; 2) escaped backslashes (\\d rather than \d).
-    _ensembl_re: "Ensembl",
-
-    # http://identifiers.org/insdc/
-    # P12345, a UniProtKB accession matches the miriam regexp but shouldn't (I think)
-    r"^([A-Z]\d{5}|[A-Z]{2}\d{6}|[A-Z]{4}\d{8}|[A-J][A-Z]{2}\d{5})(\.\d+)?$":
-    "INSDC",
-
-    # http://identifiers.org/refseq/
-    # https://www.ncbi.nlm.nih.gov/books/NBK21091/table/ch18.T.refseq_accession_numbers_and_mole/
-    r"^((AC|AP|NC|NG|NM|NP|NR|NT|NW|XM|XP|XR|YP|ZP)_\d+|(NZ\_[A-Z]{4}\d+))(\.\d+)?$":
-    "RefSeq",
-
-    # UniProtKB
-    # http://identifiers.org/uniprot/
-    # https://www.uniprot.org/help/accession_numbers
-    r"^(?:[OPQ][0-9][A-Z0-9]{3}[0-9]|[A-NR-Z][0-9]([A-Z][A-Z0-9]{2}[0-9]){1,2})$":
-    "UniProtKB",
-}
-
-ac_namespace_regexps = {re.compile(k): v for k, v in iteritems(ac_namespace_regexps)}
-
-
-def chr22XY(c):
-    """force to name from 1..22, 23, 24, X, Y, M 
-    to in chr1..chr22, chrX, chrY, chrM
-    str or ints accepted
-
-    >>> chr22XY('1')
-    'chr1'
-    >>> chr22XY(1)
-    'chr1'
-    >>> chr22XY('chr1')
-    'chr1'
-    >>> chr22XY(23)
-    'chrX'
-    >>> chr22XY(24)
-    'chrY'
-    >>> chr22XY("X")
-    'chrX'
-    >>> chr22XY("23")
-    'chrX'
-    >>> chr22XY("M")
-    'chrM'
-
-    """
-    c = str(c)
-    if c[0:3] == 'chr':
-        c = c[3:]
-    if c == '23':
-        c = 'X'
-    if c == '24':
-        c = 'Y'
-    return 'chr' + c
-
-
-def infer_namespace(ac):
-    """Infer the single namespace of the given accession
-
-    This function is convenience wrapper around infer_namespaces().
-    Returns:
-      * None if no namespaces are inferred
-      * The (single) namespace if only one namespace is inferred
-      * Raises an exception if more than one namespace is inferred
-
-    >>> infer_namespace("ENST00000530893.6")
-    'Ensembl'
-
-    >>> infer_namespace("NM_01234.5")
-    'RefSeq'
-
-    >>> infer_namespace("A2BC19")
-    'UniProtKB'
-
-    >>> infer_namespace("P12345")
-    Traceback (most recent call last):
-    ...
-    bioutils.exceptions.BioutilsError: Multiple namespaces possible for P12345
-
-    >>> infer_namespace("BOGUS99") is None
-    True
-
-    """
-
-    namespaces = infer_namespaces(ac)
-    if not namespaces:
-        return None
-    if len(namespaces) > 1:
-        raise BioutilsError("Multiple namespaces possible for {}".format(ac))
-    return namespaces[0]
-
-
-def infer_namespaces(ac):
-    """infer possible namespaces of given accession based on syntax
-    Always returns a list, possibly empty
-
-    >>> infer_namespaces("ENST00000530893.6")
-    ['Ensembl']
-    >>> infer_namespaces("ENST00000530893")
-    ['Ensembl']
-    >>> infer_namespaces("ENSQ00000530893")
-    []
-    >>> infer_namespaces("NM_01234")
-    ['RefSeq']
-    >>> infer_namespaces("NM_01234.5")
-    ['RefSeq']
-    >>> infer_namespaces("NQ_01234.5")
-    []
-    >>> infer_namespaces("A2BC19")
-    ['UniProtKB']
-    >>> sorted(infer_namespaces("P12345"))
-    ['INSDC', 'UniProtKB']
-    >>> infer_namespaces("A0A022YWF9")
-    ['UniProtKB']
-
-
-    """
-    return [v for k, v in iteritems(ac_namespace_regexps) if k.match(ac)]
-
 
 def prepend_chr(chr):
     """prefix chr with 'chr' if not present
@@ -184,15 +38,46 @@ def strip_chr(chr):
     return chr[3:] if chr[0:3] == 'chr' else chr
 
 
+def chr22XY(c):
+    """force to name from 1..22, 23, 24, X, Y, M 
+    to in chr1..chr22, chrX, chrY, chrM
+    str or ints accepted
+
+    >>> chr22XY('1')
+    'chr1'
+    >>> chr22XY(1)
+    'chr1'
+    >>> chr22XY('chr1')
+    'chr1'
+    >>> chr22XY(23)
+    'chrX'
+    >>> chr22XY(24)
+    'chrY'
+    >>> chr22XY("X")
+    'chrX'
+    >>> chr22XY("23")
+    'chrX'
+    >>> chr22XY("M")
+    'chrM'
+
+    """
+    c = str(c)
+    if c[0:3] == 'chr':
+        c = c[3:]
+    if c == '23': c = 'X'
+    if c == '24': c = 'Y'
+    return 'chr' + c
+
+
 ## <LICENSE>
 ## Copyright 2014 Bioutils Contributors (https://bitbucket.org/biocommons/bioutils)
-##
+## 
 ## Licensed under the Apache License, Version 2.0 (the "License");
 ## you may not use this file except in compliance with the License.
 ## You may obtain a copy of the License at
-##
+## 
 ##     http://www.apache.org/licenses/LICENSE-2.0
-##
+## 
 ## Unless required by applicable law or agreed to in writing, software
 ## distributed under the License is distributed on an "AS IS" BASIS,
 ## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
diff --git a/bioutils/exceptions.py b/bioutils/exceptions.py
deleted file mode 100644
index f6c5aaa..0000000
--- a/bioutils/exceptions.py
+++ /dev/null
@@ -1,3 +0,0 @@
-class BioutilsError(Exception):
-    """Root exception for all bioutils exceptions"""
-    pass
diff --git a/bioutils/seqfetcher.py b/bioutils/seqfetcher.py
index 3658542..919997f 100644
--- a/bioutils/seqfetcher.py
+++ b/bioutils/seqfetcher.py
@@ -7,6 +7,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera
 
 import logging
 import re
+import os
 
 import requests
 
@@ -203,12 +204,29 @@ def _fetch_seq_ncbi(ac, start_i=None, end_i=None):
 
     url += "&tool={tool}&email={email}".format(tool=ncbi_tool, email=ncbi_email)
 
+    url = _add_eutils_api_key(url)
+
     resp = requests.get(url)
     resp.raise_for_status()
     seq = ''.join(resp.text.splitlines()[1:])
     return seq
 
 
+
+def _add_eutils_api_key(url):
+    """Adds eutils api key to the query
+
+    :param url: eutils url with a query string
+    :return: url with api_key parameter set to the value of environment
+    variable 'ncbi_api_key' if available
+    """
+    apikey = os.environ.get('ncbi_api_key')
+    if apikey:
+        url += '&api_key={apikey}'.format(apikey=apikey)
+    return url
+
+
+
 # So that I don't forget why I didn't use ebi too:
 # $ curl 'http://www.ebi.ac.uk/ena/data/view/AM407889.1&display=fasta'
 # >ENA|AM407889|AM407889.2 Medicago sativa partial mRNA ...
diff --git a/bioutils/sequences.py b/bioutils/sequences.py
index 836e04f..f216464 100644
--- a/bioutils/sequences.py
+++ b/bioutils/sequences.py
@@ -3,14 +3,10 @@
 
 from __future__ import absolute_import, division, print_function, unicode_literals
 
-import logging
-import re
-
 import six
+import re
 
 
-_logger = logging.getLogger(__name__)
-
 aa3_to_aa1_lut = {
     "Ala": "A",
     "Arg": "R",
@@ -36,7 +32,6 @@ aa3_to_aa1_lut = {
     "Ter": "*",
     "Sec": "",
 }
-
 aa1_to_aa3_lut = {v: k for k, v in six.iteritems(aa3_to_aa1_lut)}
 
 
@@ -137,33 +132,6 @@ def complement(seq):
     return seq.translate(complement_transtable)
 
 
-def elide_sequence(s, flank=5, elision="..."):
-    """trim a sequence to include the left and right flanking sequences of
-    size `flank`, with the intervening sequence elided by `elision`.
-
-    >>> elide_sequence("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
-    'ABCDE...VWXYZ'
-
-    >>> elide_sequence("ABCDEFGHIJKLMNOPQRSTUVWXYZ", flank=3)
-    'ABC...XYZ'
-
-    >>> elide_sequence("ABCDEFGHIJKLMNOPQRSTUVWXYZ", elision="..")
-    'ABCDE..VWXYZ'
-
-    >>> elide_sequence("ABCDEFGHIJKLMNOPQRSTUVWXYZ", flank=12)
-    'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
-
-    >>> elide_sequence("ABCDEFGHIJKLMNOPQRSTUVWXYZ", flank=12, elision=".")
-    'ABCDEFGHIJKL.OPQRSTUVWXYZ'
-
-    """
-
-    elided_sequence_len = flank + flank + len(elision)
-    if len(s) <= elided_sequence_len:
-        return s
-    return s[:flank] + elision + s[-flank:]
-
-
 def looks_like_aa3_p(seq):
     """string looks like a 3-letter AA string"""
     return (seq is not None and (len(seq) % 3 == 0) and
@@ -185,18 +153,14 @@ def normalize_sequence(seq):
     >>> normalize_sequence("ACGT1")
     Traceback (most recent call last):
     ...
-    RuntimeError: Normalized sequence contains non-alphabetic characters
+    RuntimeError: normalized sequence contains non-alphabetic characters
 
     """
 
     assert isinstance(seq, six.text_type)
     nseq = re.sub("[\s\*]", "", seq).upper()
-    m = re.search("[^A-Z]", nseq)
-    if m:
-        _logger.debug("Original sequence: " + seq)
-        _logger.debug("Normalized sequence: " + nseq)
-        _logger.debug("First non-[A-Z] at {}".format(m.start()))
-        raise RuntimeError("Normalized sequence contains non-alphabetic characters")
+    if re.search("[^A-Z]", nseq):
+        raise RuntimeError("normalized sequence contains non-alphabetic characters")
     return nseq
 
 
diff --git a/circle.yml b/circle.yml
new file mode 100644
index 0000000..fdd64ec
--- /dev/null
+++ b/circle.yml
@@ -0,0 +1,3 @@
+machine:
+  post:
+    - pyenv global 2.7.12 3.5.3


Test patch:
diff --git a/tests/test_seqfetcher.py b/tests/test_seqfetcher.py
index 4d44847..6154fa6 100644
--- a/tests/test_seqfetcher.py
+++ b/tests/test_seqfetcher.py
@@ -1,7 +1,8 @@
 import pytest
 import vcr
+import os
 
-from bioutils.seqfetcher import fetch_seq, _fetch_seq_ensembl, _fetch_seq_ncbi
+from bioutils.seqfetcher import fetch_seq, _fetch_seq_ensembl, _fetch_seq_ncbi, _add_eutils_api_key
 
 
 @vcr.use_cassette
@@ -21,7 +22,20 @@ def test_fetch_seq():
     assert 'TTTATTTATTTTAGATACTTATCTC' == fetch_seq('KB663603.1', 0, 25)
     assert 'CGCCTCCCTTCCCCCTCCCCGCCCG' == fetch_seq('ENST00000288602', 0, 25)
     assert 'MAALSGGGGGGAEPGQALFNGDMEP' == fetch_seq('ENSP00000288602', 0, 25)
-    
+
+
+def test_add_eutils_api_key():
+    try:
+        url = 'http://test.com?boo=bar'
+        assert _add_eutils_api_key(url) == url
+        os.environ['ncbi_api_key'] = 'test-api-key'
+        assert _add_eutils_api_key(url) == url + '&api_key=test-api-key'
+    finally:
+        try:
+            os.environ.pop('ncbi_api_key')
+        except KeyError:
+            pass
+
 
 def test_fetch_seq_errors():
     # Traceback (most recent call last):