diff --git a/.travis.yml b/.travis.yml index c579288f..af2c1dde 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,9 @@ language: python python: - "2.7" - - "3.4" install: - - cd lib - - pip install coveralls - - pip install -r requirements.txt - - cd .. + - "pip install -r requirements-travis.txt" script: - cd lib diff --git a/docs/conf.py b/docs/conf.py index 2d2a88d3..a0b7521a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,6 +31,7 @@ # ones. extensions = [ 'sphinx.ext.autodoc', + 'sphinxcontrib.napoleon', 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx.ext.coverage', diff --git a/docs/index.rst b/docs/index.rst index 617564af..e1e0a990 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,16 +3,76 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to lib's documentation! -=============================== +KBase Data API documentation +============================ +The Data API provides a unified entry point to retrieve and, eventually, +store KBase data objects. -Contents: +On this page +------------ + +.. contents:: + + :depth: 1 + +API Reference +------------- .. toctree:: - :maxdepth: 4 + :maxdepth: 2 + + biokbase.data_api + +Using the Data API +================== + +There are two primary modes of using the Data API: interactively and programmatically. +Interactively, the API can be imported into in the IPython/Jupyter notebook or +Narrative and used to explore and examine data objects. +Results will be automatically displayed as HTML and inline plots. Programmatically, +the same API can be imported into any Python code and used like a standard +library. + +.. note:: + + The integration of the Data API into the KBase Narrative is not quite + done yet. For now, you need to try it out in a recent (August 2015+) + version of the Jupyter notebook. + +In both cases, the :ref:`core-api` functions are used to access the objects. +For interactive use, and some programmatic use-cases, the :ref:`highlevel-api` +will be more convenient. + +.. _highlevel-api: + +High-level API +-------------- +This section covers how to :ref:`initialize the high-level API ` +to access the KBase data as an authenticated user, +then how to :ref:`use the provided functions `. + +.. _highlevel-api-conf: + +Configuration and Authorization ++++++++++++++++++++++++++++++++ + +.. _highlevel-api-func: + +Functions ++++++++++ + +.. _core-api: + +Core API +-------- +This section covers how to initialize the high-level API to access the KBase +data as an authenticated user, then how to use the provided functions. - biokbase +Configuration and Authorization ++++++++++++++++++++++++++++++++ +Functions ++++++++++ Indices and tables ================== diff --git a/docs/matplotlibrc b/docs/matplotlibrc new file mode 100644 index 00000000..3607bbd1 --- /dev/null +++ b/docs/matplotlibrc @@ -0,0 +1 @@ +backend : PDF diff --git a/lib/biokbase/data_api/assembly.py b/lib/biokbase/data_api/assembly.py index 45e1ed48..6d65dd26 100644 --- a/lib/biokbase/data_api/assembly.py +++ b/lib/biokbase/data_api/assembly.py @@ -24,13 +24,11 @@ TYPES = _CONTIGSET_TYPES + _ASSEMBLY_TYPES class AssemblyAPI(ObjectAPI): - """ - API for the assembled sequences associated with a Genome Annotation. + """API for the assembled sequences associated with a Genome Annotation. """ def __init__(self, services, ref): - """ - Defines which types and type versions that are legal. + """Defines which types and type versions that are legal. """ super(AssemblyAPI, self).__init__(services, ref) @@ -41,8 +39,7 @@ def __init__(self, services, ref): raise TypeError("Invalid type! Expected one of {0}, received {1}".format(TYPES, self._typestring)) def get_assembly_id(self): - """ - Retrieve the id for an Assembly. + """Retrieve the id for an Assembly. Returns: id: string identifier for the Assembly""" @@ -53,11 +50,11 @@ def get_assembly_id(self): return self.get_data_subset(path_list=["assembly_id"])["assembly_id"] def get_genome_annotations(self): - """ - Retrieve the GenomeAnnotations that refer to this Assembly. + """Retrieve the GenomeAnnotations that refer to this Assembly. Returns: - list""" + list + """ import biokbase.data_api.genome_annotation @@ -67,7 +64,9 @@ def get_genome_annotations(self): for object_type in referrers: if object_type.split('-')[0] in biokbase.data_api.genome_annotation.TYPES: for x in referrers[object_type]: - annotations.append(GenomeAnnotationAPI(self.services, ref=x)) + annotations.append( + biokbase.data_api.genome_annotation.GenomeAnnotationAPI( + self.services, ref=x)) if len(annotations) == 0: return None @@ -75,8 +74,7 @@ def get_genome_annotations(self): return annotations def get_external_source_info(self): - """ - Retrieve the external source information associated with this Assembly. + """Retrieve the external source information associated with this Assembly. Returns: id: string identifier for the Assembly""" @@ -95,8 +93,7 @@ def get_external_source_info(self): return output def get_stats(self): - """ - Retrieve the derived statistical information about this Assembly. + """Retrieve the derived statistical information about this Assembly. Returns: gc_content: total guanine and cytosine content, counting all G and C only @@ -112,7 +109,7 @@ def get_stats(self): for c in contigs: total_gc += len([s for s in re.finditer(pattern, c["sequence"])]) - total_length = sum([x.length in contigs]) + total_length = sum([x.length for x in contigs]) data = dict() data["gc_content"] = total_gc/(total_length*1.0) @@ -123,8 +120,7 @@ def get_stats(self): return self.get_data_subset(path_list=["gc_content","dna_size","num_contigs"]) def get_number_contigs(self): - """ - Retrieve the number of contiguous sequences in this Assembly. + """Retrieve the number of contiguous sequences in this Assembly. Returns: int""" @@ -135,8 +131,7 @@ def get_number_contigs(self): return self.get_data_subset(path_list=["num_contigs"])["num_contigs"] def get_gc_content(self): - """ - Retrieve the total GC content for this Assembly. + """Retrieve the total GC content for this Assembly. Returns: float""" @@ -157,8 +152,7 @@ def get_gc_content(self): return self.get_data_subset(path_list=["gc_content"])["gc_content"] def get_dna_size(self): - """ - Retrieve the total DNA size for this Assembly. + """Retrieve the total DNA size for this Assembly. Returns: int""" @@ -170,8 +164,7 @@ def get_dna_size(self): return self.get_data_subset(path_list=["dna_size"])["dna_size"] def get_contig_lengths(self, contig_id_list=None): - """ - Retrieve the ids for every contiguous sequence in this Assembly. + """Retrieve the ids for every contiguous sequence in this Assembly. Returns: dict: """ @@ -191,8 +184,7 @@ def get_contig_lengths(self, contig_id_list=None): return result def get_contig_gc_content(self, contig_id_list=None): - """ - Retrieve the total GC content for each contiguous sequence of this Assembly. + """Retrieve the total GC content for each contiguous sequence of this Assembly. Returns: dict: float""" @@ -222,8 +214,7 @@ def get_contig_gc_content(self, contig_id_list=None): return contigs_gc def get_contig_ids(self): - """ - Retrieve the ids for every contiguous sequence in this Assembly. + """Retrieve the ids for every contiguous sequence in this Assembly. Returns: list""" @@ -237,8 +228,7 @@ def get_contig_ids(self): return result def get_contigs_by_id(self, contig_id_list=None): - """ - Retrieve contiguous sequences from this Assembly by id. + """Retrieve contiguous sequences from this Assembly by id. Args: contig_id_list: list @@ -246,20 +236,20 @@ def get_contigs_by_id(self, contig_id_list=None): dict dictionary of contigs, with contig id as key - contig value structure - { - 'contig_id': string, - 'length': integer, - 'md5': string, - 'name': string, - 'description': string, - 'is_complete': 0 or 1, - 'is_circular': 0 or 1, - 'sequence': string - } + contig value structure:: + + { + 'contig_id': string, + 'length': integer, + 'md5': string, + 'name': string, + 'description': string, + 'is_complete': 0 or 1, + 'is_circular': 0 or 1, + 'sequence': string + } """ - - if contig_id_list == None: + if contig_id_list is None: contig_id_list = self.get_data()["contigs"] if self._is_contigset_type: diff --git a/lib/biokbase/data_api/object.py b/lib/biokbase/data_api/object.py index 26fd23fc..4dbad36c 100644 --- a/lib/biokbase/data_api/object.py +++ b/lib/biokbase/data_api/object.py @@ -21,21 +21,33 @@ def get_token(): "Missing authentication token! Set KB_AUTH_TOKEN environment variable.") class ObjectAPI(object): - """ - Generic Object API for basic properties and actions of a KBase Data Object. + """Generic Object API for basic properties and actions + of a KBase Data Object. """ def __init__(self, services=None, ref=None): - if services == None or type(services) != type({}): + """Create new object. + + Args: + services (dict): Service configuration dictionary. Required keys: + * workspace_service_url: URL for Workspace, such as + `https://ci.kbase.us/services/ws/` + ref (str): Object reference, which can be the name of the object + (although this is not unique), or a numeric identifier in the + format `A/B[/C]` where A is the number of the workspace, B is the + number identifying the object, and C is the "version" number of + the object. + """ + if services is None or type(services) != type({}): raise TypeError("You must provide a service configuration dictionary! Found {0}".format(type(services))) elif not services.has_key("workspace_service_url"): raise KeyError("Expecting workspace_service_url key!") - if ref == None: + if ref is None: raise TypeError("Missing object reference!") elif type(ref) != type("") and type(ref) != type(unicode()): raise TypeError("Invalid reference given, expected string! Found {0}".format(type(ref))) - elif re.match(REF_PATTERN, ref) == None: + elif re.match(REF_PATTERN, ref) is None: raise TypeError("Invalid workspace reference string! Found {0}".format(ref)) self.services = services @@ -97,7 +109,8 @@ def get_info(self): Retrieve basic properties about this object. Returns: - dict""" + dict + """ return self._info diff --git a/lib/biokbase/data_api/tests/__init__.py b/lib/biokbase/data_api/tests/__init__.py index c9b644a7..545aab33 100644 --- a/lib/biokbase/data_api/tests/__init__.py +++ b/lib/biokbase/data_api/tests/__init__.py @@ -1,12 +1,12 @@ -def basic_suite(): - import biokbase.data_api.tests.test_suite_basic - biokbase.data_api.tests.test_suite_basic.test_assembly_api() - biokbase.data_api.tests.test_suite_basic.test_genome_annotation_api() - biokbase.data_api.tests.test_suite_basic.test_taxon_api() - -def extended_suite(): - import biokbase.data_api.tests.test_suite_extended - biokbase.data_api.tests.test_suite_extended.test_assembly_api() - biokbase.data_api.tests.test_suite_extended.test_genome_annotation_api() - biokbase.data_api.tests.test_suite_extended.test_taxon_api() - \ No newline at end of file +# def basic_suite(): +# import biokbase.data_api.tests.test_suite_basic +# biokbase.data_api.tests.test_suite_basic.test_assembly_api() +# biokbase.data_api.tests.test_suite_basic.test_genome_annotation_api() +# biokbase.data_api.tests.test_suite_basic.test_taxon_api() +# +# def extended_suite(): +# import biokbase.data_api.tests.test_suite_extended +# biokbase.data_api.tests.test_suite_extended.test_assembly_api() +# biokbase.data_api.tests.test_suite_extended.test_genome_annotation_api() +# biokbase.data_api.tests.test_suite_extended.test_taxon_api() +# \ No newline at end of file diff --git a/lib/biokbase/data_api/tests/shared.py b/lib/biokbase/data_api/tests/shared.py new file mode 100644 index 00000000..62aaa9c6 --- /dev/null +++ b/lib/biokbase/data_api/tests/shared.py @@ -0,0 +1,28 @@ +""" +Shared by test code +""" +__author__ = 'Dan Gunter ' +__date__ = '8/11/15' + +import logging + +from biokbase.data_api.assembly import AssemblyAPI + +can_connect = False +services = { + "workspace_service_url": "https://ci.kbase.us/services/ws/", + "shock_service_url": "https://ci.kbase.us/services/shock-api/", +} +genome = 'PrototypeReferenceGenomes/kb|g.3157' + +def setup(): + global can_connect + + logging.basicConfig() + _log = logging.getLogger() + + try: + _ = AssemblyAPI(services=services, ref=genome + '_assembly') + can_connect = True + except: + _log.warn('Cannot connect to workspace! Most tests will be skipped') diff --git a/lib/biokbase/data_api/tests/test_genome_annotation_api.py b/lib/biokbase/data_api/tests/test_genome_annotation_api.py index a7592c28..3da4b599 100644 --- a/lib/biokbase/data_api/tests/test_genome_annotation_api.py +++ b/lib/biokbase/data_api/tests/test_genome_annotation_api.py @@ -1,113 +1,85 @@ +""" +Unit tests for genome_annotation +""" +import logging +from unittest import skipUnless + +from . import shared + +from biokbase.data_api.genome_annotation import GenomeAnnotationAPI + +_log = logging.getLogger(__name__) + +genome = "PrototypeReferenceGenomes/kb|g.3899" + +def setup(): + shared.setup() + +@skipUnless(shared.can_connect, 'Cannot connect to workspace') def test_get_cds_by_mrna(): - print "Testing Genome Annotation API" - print "Fetching CDS by kb|g.3899.mRNA.0, kb|g.3899.mRNA.2066, kb|g.3899.mRNA.99999999999, kb|g.3899.CDS.1" - - from biokbase.data_api.genome_annotation import GenomeAnnotationAPI - - services = { - "workspace_service_url": "https://ci.kbase.us/services/ws/", - "shock_service_url": "https://ci.kbase.us/services/shock-api/", - } - - ci_genome_annotation_api = GenomeAnnotationAPI(services=services, ref="PrototypeReferenceGenomes/kb|g.3899") + """Testing Genome Annotation API""" + + _log.info("Fetching CDS by kb|g.3899.mRNA.0, kb|g.3899.mRNA.2066, kb|g.3899.mRNA.99999999999, kb|g.3899.CDS.1") + + ci_genome_annotation_api = GenomeAnnotationAPI(services=shared.services, + ref=genome) #last 2 in list are purposely invalid subset_features = ci_genome_annotation_api.get_cds_by_mrna(["kb|g.3899.mRNA.0","kb|g.3899.mRNA.2066", "kb|g.3899.mRNA.99999999999", "kb|g.3899.CDS.1"]) print subset_features assert len(subset_features) == 2 - - - - +@skipUnless(shared.can_connect, 'Cannot connect to workspace') def test_get_mrna_by_cds(): - print "Testing Genome Annotation API" + """Testing Genome Annotation API""" print "Fetching mRNA by kb|g.3899.CDS.61899, kb|g.3899.CDS.63658, kb|g.3899.mRNA.99999999999, kb|g.3899.CDS.1" - - from biokbase.data_api.genome_annotation import GenomeAnnotationAPI - - services = { - "workspace_service_url": "https://ci.kbase.us/services/ws/", - "shock_service_url": "https://ci.kbase.us/services/shock-api/", - } - - ci_genome_annotation_api = GenomeAnnotationAPI(services=services, ref="PrototypeReferenceGenomes/kb|g.3899") + ci_genome_annotation_api = GenomeAnnotationAPI(services=shared.services, + ref=genome) #last 2 in list are purposely invalid subset_features = ci_genome_annotation_api.get_mrna_by_cds(["kb|g.3899.CDS.36740","kb|g.3899.CDS.36739", "kb|g.3899.mRNA.99999999999", "kb|g.3899.CDS.1"]) print subset_features assert len(subset_features) == 2 - - +@skipUnless(shared.can_connect, 'Cannot connect to workspace') def test_get_gene_by_mrna(): - print "Testing Genome Annotation API" + """Testing Genome Annotation API""" print "Fetching genes by kb|g.3899.mRNA.0, kb|g.3899.mRNA.2066, kb|g.3899.mRNA.99999999999, kb|g.3899.CDS.1" - - from biokbase.data_api.genome_annotation import GenomeAnnotationAPI - - services = { - "workspace_service_url": "https://ci.kbase.us/services/ws/", - "shock_service_url": "https://ci.kbase.us/services/shock-api/", - } - - ci_genome_annotation_api = GenomeAnnotationAPI(services=services, ref="PrototypeReferenceGenomes/kb|g.3899") + + ci_genome_annotation_api = GenomeAnnotationAPI(services=shared.services, + ref=genome) #last 2 in list are purposely invalid subset_features = ci_genome_annotation_api.get_gene_by_mrna(["kb|g.3899.mRNA.0","kb|g.3899.mRNA.2066", "kb|g.3899.mRNA.99999999999", "kb|g.3899.CDS.1"]) print subset_features assert len(subset_features) == 2 - - - - +@skipUnless(shared.can_connect, 'Cannot connect to workspace') def test_get_children_cds_by_gene(): - print "Testing Genome Annotation API" + """Testing Genome Annotation API""" print "Fetching CDS by gene kb|g.3899.locus.26937,kb|g.3899.locus.26761, kb|g.3899.mRNA.99999999999, kb|g.3899.CDS.1" - - from biokbase.data_api.genome_annotation import GenomeAnnotationAPI - - services = { - "workspace_service_url": "https://ci.kbase.us/services/ws/", - "shock_service_url": "https://ci.kbase.us/services/shock-api/", - } - - ci_genome_annotation_api = GenomeAnnotationAPI(services=services, ref="PrototypeReferenceGenomes/kb|g.3899") + + ci_genome_annotation_api = GenomeAnnotationAPI(services=shared.services, ref=genome) #last 2 in list are purposely invalid subset_features = ci_genome_annotation_api.get_children_cds_by_gene(["kb|g.3899.locus.26937","kb|g.3899.locus.26761", "kb|g.3899.mRNA.99999999999", "kb|g.3899.CDS.1"]) print subset_features assert len(subset_features) == 2 - +@skipUnless(shared.can_connect, 'Cannot connect to workspace') def test_get_children_mrna_by_gene(): - print "Testing Genome Annotation API" + """Testing Genome Annotation API""" print "Fetching mRNA by gene kb|g.3899.locus.26937, kb|g.3899.locus.26761, kb|g.3899.mRNA.99999999999, kb|g.3899.CDS.1" - - from biokbase.data_api.genome_annotation import GenomeAnnotationAPI - - services = { - "workspace_service_url": "https://ci.kbase.us/services/ws/", - "shock_service_url": "https://ci.kbase.us/services/shock-api/", - } - - ci_genome_annotation_api = GenomeAnnotationAPI(services=services, ref="PrototypeReferenceGenomes/kb|g.3899") + + ci_genome_annotation_api = GenomeAnnotationAPI(services=shared.services, ref=genome) #last 2 in list are purposely invalid subset_features = ci_genome_annotation_api.get_children_mrna_by_gene(["kb|g.3899.locus.26937","kb|g.3899.locus.26761", "kb|g.3899.mRNA.99999999999", "kb|g.3899.CDS.1"]) print subset_features assert len(subset_features) == 2 - - +@skipUnless(shared.can_connect, 'Cannot connect to workspace') def test_get_gene_by_cds(): - print "Testing Genome Annotation API" + """Testing Genome Annotation API""" print "Fetching genes by kb|g.3899.CDS.61899, kb|g.3899.CDS.63658, kb|g.3899.mRNA.99999999999, kb|g.3899.CDS.1" - from biokbase.data_api.genome_annotation import GenomeAnnotationAPI - - services = { - "workspace_service_url": "https://ci.kbase.us/services/ws/", - "shock_service_url": "https://ci.kbase.us/services/shock-api/", - } - - ci_genome_annotation_api = GenomeAnnotationAPI(services=services, ref="PrototypeReferenceGenomes/kb|g.3899") + + ci_genome_annotation_api = GenomeAnnotationAPI(services=shared.services, ref=genome) #last 2 in list are purposely invalid subset_features = ci_genome_annotation_api.get_gene_by_cds(["kb|g.3899.CDS.36740","kb|g.3899.CDS.36739", "kb|g.3899.mRNA.99999999999", "kb|g.3899.CDS.1"]) print subset_features diff --git a/lib/biokbase/data_api/tests/test_nav.py b/lib/biokbase/data_api/tests/test_nav.py index 58de0a2e..d790d8bb 100644 --- a/lib/biokbase/data_api/tests/test_nav.py +++ b/lib/biokbase/data_api/tests/test_nav.py @@ -7,6 +7,11 @@ import unittest from biokbase.data_api import nav +from . import shared + +def setup(): + shared.setup() + class MockConn(nav.DBConnection): """Mock database connection. """ @@ -20,16 +25,8 @@ def list_objects(self): def get_object(self, objid): return None -class NavTests(unittest.TestCase): - def setUp(self): - conn = MockConn('myWorkspace') - self.finder = nav.Finder(conn) - - def testLs(self): - self.failUnless(False) # Fail - -def main(): - unittest.main() - -if __name__ == '__main__': - main() +@unittest.skipUnless(shared.can_connect, 'Cannot connect to workspace') +def test_list(): + """Test the 'ls' functions.""" + conn = MockConn('myWorkspace') + finder = nav.Finder(conn) diff --git a/lib/biokbase/data_api/tests/test_suite_basic.py b/lib/biokbase/data_api/tests/test_suite_basic.py index 6dea5051..118af7ce 100644 --- a/lib/biokbase/data_api/tests/test_suite_basic.py +++ b/lib/biokbase/data_api/tests/test_suite_basic.py @@ -1,53 +1,50 @@ +""" +Basic unit test suite. +""" +import logging +from unittest import skipUnless + +from . import shared + +from biokbase.data_api.assembly import AssemblyAPI +from biokbase.data_api.genome_annotation import GenomeAnnotationAPI + +_log = logging.getLogger(__name__) + def setup(): - pass + shared.setup() def teardown(): pass +@skipUnless(shared.can_connect, 'Cannot connect to workspace') def test_assembly_api(): - print "Testing Assembly API" - print "Fetching kb|g.3157.c.0" - - from biokbase.data_api.assembly import AssemblyAPI - - services = { - "workspace_service_url": "https://ci.kbase.us/services/ws/", - "shock_service_url": "https://ci.kbase.us/services/shock-api/", - } - - ci_assembly_api = AssemblyAPI(services=services, ref="PrototypeReferenceGenomes/kb|g.3157_assembly") + """Testing Assembly API""" + _log.info("Fetching kb|g.3157.c.0") + ci_assembly_api = AssemblyAPI(services=shared.services, + ref=shared.genome + "_assembly") subset_contigs = ci_assembly_api.get_contigs_by_id(["kb|g.3157.c.0"]) - print subset_contigs + _log.debug("Got contigs: {}".format(subset_contigs)) assert len(subset_contigs) == 1 +@skipUnless(shared.can_connect, 'Cannot connect to workspace') def test_genome_annotation_api(): - print "Testing Genome Annotation API" + """Testing Genome Annotation API""" print "Fetching kb|g.3157.peg.0" - from biokbase.data_api.genome_annotation import GenomeAnnotationAPI - services = { - "workspace_service_url": "https://ci.kbase.us/services/ws/", - "shock_service_url": "https://ci.kbase.us/services/shock-api/", - } - - ci_genome_annotation_api = GenomeAnnotationAPI(services=services, ref="PrototypeReferenceGenomes/kb|g.3157") + ci_genome_annotation_api = GenomeAnnotationAPI(services=shared.services, + ref=shared.genome) subset_features = ci_genome_annotation_api.get_features_by_id(["kb|g.3157.peg.0"]) print subset_features assert len(subset_features) == 1 +@skipUnless(shared.can_connect, 'Cannot connect to workspace') def test_taxon_api(): - print "Testing Taxon API" - print "Fetching taxon for kb|g.3157" - - from biokbase.data_api.genome_annotation import GenomeAnnotationAPI - - services = { - "workspace_service_url": "https://ci.kbase.us/services/ws/", - "shock_service_url": "https://ci.kbase.us/services/shock-api/", - } - - ci_taxon_api = GenomeAnnotationAPI(services=services, ref="PrototypeReferenceGenomes/kb|g.3157").get_taxon() + """Testing Taxon API""" + _log.info("Fetching taxon for kb|g.3157") + ci_taxon_api = GenomeAnnotationAPI(services=shared.services, + ref=shared.genome).get_taxon() scientific_name = ci_taxon_api.get_scientific_name() print scientific_name assert len(scientific_name) > 0 diff --git a/requirements-travis.txt b/requirements-travis.txt new file mode 100644 index 00000000..da58270f --- /dev/null +++ b/requirements-travis.txt @@ -0,0 +1,5 @@ +nose +python-coveralls +six==1.9.0 +enum34 +requests==2.7.0 diff --git a/requirements.txt b/requirements.txt index 4f38dfc3..0cc844f4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ Jinja2==2.7.3 Sphinx==1.3.1 +sphinxcontrib-napoleon==0.3.11 guzzle-sphinx-theme==0.7.2 nose python-coveralls