Skip to content

Commit

Permalink
feat: add functions to call fast WoRMS API directly
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinsbarnard committed Feb 6, 2024
1 parent 8e0d760 commit 21942b8
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 0 deletions.
97 changes: 97 additions & 0 deletions fathomnet/api/worms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from typing import List

from fathomnet.api import EndpointManager
from fathomnet.dto import WormsNode


class Worms(EndpointManager):
ROOT = 'https://fathomnet.org'
PATH = 'worms'


def count_names() -> int:
"""Get the total number of names available."""
return int(Worms.get('names/count'))


def get_all_names(limit: int = 100, offset: int = 0) -> List[str]:
"""Get all names."""
res_json = Worms.get('names', params={'limit': limit, 'offset': offset})
return res_json['items']


def get_ancestors_names(name: str) -> List[str]:
"""Get all ancestors' names of a given name."""
return Worms.get(f'ancestors/{name}')


def get_children_names(name: str) -> List[str]:
"""Get all children's names of a given name."""
return Worms.get(f'children/{name}')


def get_descendants_names(name: str) -> List[str]:
"""Get all descendants' names of a given name."""
return Worms.get(f'descendants/{name}')


def get_parent_name(name: str) -> str:
"""Get the parent's name of a given name."""
return Worms.get(f'parent/{name}')


def find_names_containing(fragment: str) -> List[str]:
"""Get all names that contain a fragment."""
return Worms.get(f'query/contains/{fragment}')


def find_names_by_prefix(prefix: str) -> List[str]:
"""Get all names that start with a prefix."""
return Worms.get(f'query/startswith/{prefix}')


def get_synonyms_for_name(name: str) -> List[str]:
"""Get all synonyms for a name."""
return Worms.get(f'synonyms/{name}')


def get_ancestors(name: str) -> WormsNode:
"""Get a taxa tree from the root node to the node for the given name."""
res_json = Worms.get(f'taxa/ancestors/{name}')
return WormsNode.from_dict(res_json)


def get_children(name: str) -> List[WormsNode]:
"""Get the child taxa nodes of a given name."""
res_json = Worms.get(f'taxa/children/{name}')
return [WormsNode.from_dict(item) for item in res_json]


def get_descendants(name: str) -> WormsNode:
"""Get a taxa tree from the given name to the leaves."""
res_json = Worms.get(f'taxa/descendants/{name}')
return WormsNode.from_dict(res_json)


def get_parent(name: str) -> WormsNode:
"""Get the parent taxa node of a given name."""
res_json = Worms.get(f'taxa/parent/{name}')
return WormsNode.from_dict(res_json)


def get_info(name: str) -> WormsNode:
"""Get a taxa node for a given name."""
res_json = Worms.get(f'taxa/info/{name}')
return WormsNode.from_dict(res_json)


def find_taxa_by_prefix(prefix: str, rank: str = None, parent: str = None) -> List[WormsNode]:
"""Get all taxa nodes that start with a prefix."""
params = {}
if rank is not None:
params['rank'] = rank
if parent is not None:
params['parent'] = parent

res_json = Worms.get(f'taxa/query/startswith/{prefix}', params=params)
return [WormsNode.from_dict(item) for item in res_json]
10 changes: 10 additions & 0 deletions fathomnet/dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -600,3 +600,13 @@ class FollowedTopic(Topic):
email: Optional[str] = None
createdTimestamp: Optional[str] = None
lastUpdatedTimestamp: Optional[str] = None


@dataclass_json
@dataclass
class WormsNode:
name: Optional[str] = None
rank: Optional[str] = None
apiaId: Optional[int] = None
alternateNames: Optional[List[str]] = None
children: Optional[List['WormsNode']] = None
99 changes: 99 additions & 0 deletions test/test_worms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
from unittest import TestCase

from fathomnet.api import worms
from fathomnet.dto import WormsNode


def check_for_name(node: WormsNode, name: str) -> bool:
"""Recursively check if a node or one of its descendants has a given name."""
def recurse_check(node: WormsNode) -> bool:
print(node.name, name)
if node.name == name:
return True
if not node.children:
return False
return any(recurse_check(child) for child in node.children)
return recurse_check(node)


class TestWormsAPI(TestCase):

def test_count_names(self):
count = worms.count_names()
self.assertIsNotNone(count)
self.assertGreater(count, 0)

def test_get_all_names(self):
names = worms.get_all_names()
self.assertIsNotNone(names)

def test_get_ancestors_names(self):
ancestors = worms.get_ancestors_names('Animalia')
self.assertIsNotNone(ancestors)
self.assertIn('object', ancestors)

def test_get_children_names(self):
children = worms.get_children_names('Bathochordaeus')
self.assertIsNotNone(children)
self.assertIn('Bathochordaeus charon', children)

def test_get_descendants_names(self):
descendants = worms.get_descendants_names('Bathochordaeus')
self.assertIsNotNone(descendants)
self.assertIn('Bathochordaeus charon', descendants)

def test_get_parent_name(self):
parent = worms.get_parent_name('Bathochordaeus charon')
self.assertIsNotNone(parent)
self.assertEqual('Bathochordaeus', parent)

def test_find_names_containing(self):
names = worms.find_names_containing('pendicula')
self.assertIsNotNone(names)
self.assertIn('Appendicularia', names)

def test_find_names_by_prefix(self):
names = worms.find_names_by_prefix('Appendicula')
self.assertIsNotNone(names)
self.assertIn('Appendicularia', names)

def test_get_synonyms_for_name(self):
synonyms = worms.get_synonyms_for_name('Appendicularia')
self.assertIsNotNone(synonyms)
self.assertIn('larvaceans', synonyms)

def test_get_ancestors(self):
root_node = worms.get_ancestors('Animalia')
self.assertIsNotNone(root_node)
self.assertTrue(check_for_name(root_node, 'object'), 'No "object" node found in ancestors')

def test_get_children(self):
children = worms.get_children('Bathochordaeus')
self.assertIsNotNone(children)
for child in children:
if child.name == 'Bathochordaeus charon':
return
self.fail('No "Bathochordaeus charon" child found')

def test_get_descendants(self):
taxa_node = worms.get_descendants('Bathochordaeus')
self.assertIsNotNone(taxa_node)
self.assertTrue(check_for_name(taxa_node, 'Bathochordaeus charon'), 'No "Bathochordaeus charon" descendant found')

def test_get_parent(self):
parent = worms.get_parent('Bathochordaeus charon')
self.assertIsNotNone(parent)
self.assertEqual('Bathochordaeus', parent.name)

def test_get_info(self):
info = worms.get_info('Bathochordaeus charon')
self.assertIsNotNone(info)
self.assertEqual('Bathochordaeus charon', info.name)

def test_find_taxa_by_prefix(self):
taxa = worms.find_taxa_by_prefix('Appendicula')
self.assertIsNotNone(taxa)
for taxon in taxa:
if taxon.name == 'Appendicularia':
return
self.fail('No "Appendicularia" taxon found')

0 comments on commit 21942b8

Please sign in to comment.