Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
7b7f35f
Add the schema validation functions to __init__
amanchoudhri Jul 29, 2020
f3c9f51
Require pymongo with pybel installation
amanchoudhri Jul 29, 2020
beefafa
Create exporter and basic query functions
amanchoudhri Jul 29, 2020
9965e00
Fix imports
amanchoudhri Jul 29, 2020
ee366a6
Reorder imports
amanchoudhri Jul 29, 2020
dc9bc64
Break up get_edges into two smaller functions
amanchoudhri Jul 30, 2020
cd4715f
Pass flake8
amanchoudhri Jul 30, 2020
affbf35
Restructure exporter from a folder to a file
amanchoudhri Jul 30, 2020
414e58c
Start writing tests
amanchoudhri Jul 30, 2020
d0b7b8d
Add test for edge presence in collection
amanchoudhri Jul 30, 2020
6d3ce66
Add test for find_nodes() query function
amanchoudhri Jul 31, 2020
396855e
Start a mongodb server during Travis testing
amanchoudhri Jul 31, 2020
d0f489b
Relocate mongo server connection to to_mongodb()
amanchoudhri Aug 1, 2020
dbdfa30
Add mongo connection to setUp, skip tests on error
amanchoudhri Aug 1, 2020
abb8d72
Add function to remove MongoDB key entries
amanchoudhri Aug 1, 2020
c63410a
Add tests for getting edges from node/criteria
amanchoudhri Aug 1, 2020
67ea70d
Remove extraneous 'self' param from _rm_mongo_keys
amanchoudhri Aug 1, 2020
0f243e6
Finalize tests for retrieving edges
amanchoudhri Aug 1, 2020
5822982
Merge branch 'master' into add-mongo-exporter
amanchoudhri Aug 2, 2020
ddf72df
Remove unnecessary _handle_member function
amanchoudhri Aug 3, 2020
86e6f29
Debug and rework edge tests for SBEL data format
amanchoudhri Aug 3, 2020
2adc02a
Merge branch 'add-mongo-exporter' of https://github.com/pybel/pybel i…
amanchoudhri Aug 3, 2020
b8aac7b
Clean up imports
amanchoudhri Aug 3, 2020
7ecd3ea
Make SBEL more functional
cthoyt Aug 4, 2020
585ab24
Merge 'add-mongo-exporter' into branch
amanchoudhri Aug 4, 2020
b9536f2
Modify test to include new to_sbel functionality
amanchoudhri Aug 4, 2020
e3d81c9
Add option to search for nodes based on function
amanchoudhri Aug 5, 2020
29638bd
Pass flake8
amanchoudhri Aug 5, 2020
025a404
Merge branch 'master' into add-mongo-exporter
amanchoudhri Aug 6, 2020
d23ef28
Remove schema imports from top level
amanchoudhri Aug 6, 2020
5231ba7
Make imports relative
amanchoudhri Aug 6, 2020
c8e5fef
Relocate _entity_to_dict() to the tests file
amanchoudhri Aug 6, 2020
b091b6f
Replace string literals with pybel constants
amanchoudhri Aug 6, 2020
2511ede
Add default db/collection and bulk insert entries
amanchoudhri Aug 6, 2020
217b2fa
Merge branch 'add-mongo-exporter' of https://github.com/pybel/pybel i…
amanchoudhri Aug 6, 2020
6d4968b
Pass flake8
amanchoudhri Aug 6, 2020
dc2babe
Add flake8 check to tests
cthoyt Aug 12, 2020
1aab477
Merge branch 'master' into add-mongo-exporter
cthoyt Aug 14, 2020
fb503e0
Fix names
cthoyt Aug 14, 2020
eed415a
Return nodes as pybel classes
cthoyt Aug 14, 2020
6aa6b66
Update test_mongodb.py
cthoyt Aug 14, 2020
c829fbc
Parse found nodes to dsl
amanchoudhri Aug 16, 2020
19f3407
Merge branch 'add-mongo-exporter' of https://github.com/pybel/pybel i…
amanchoudhri Aug 16, 2020
f18d1f6
Parse results of found_node() to DSL entities
amanchoudhri Aug 17, 2020
3da351b
Pass flake8
amanchoudhri Aug 17, 2020
0c4c2c8
Pass flake8
amanchoudhri Aug 17, 2020
f318f4a
Improve documentation and add code examples
amanchoudhri Aug 17, 2020
ce37753
Fix loop where a dict changes size mid-iteration
amanchoudhri Feb 10, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ dist: xenial
os: linux

services:
- mongodb
- mysql
- postgresql

Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ install_requires =
tabulate
pandas
jsonschema
pymongo

# Random options
zip_safe = false
Expand Down
1 change: 1 addition & 0 deletions src/pybel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from .io.umbrella_nodelink import to_umbrella_nodelink, to_umbrella_nodelink_file, to_umbrella_nodelink_gz
from .manager import Manager, from_database, to_database
from .parser.parse_bel import parse
from .schema import is_valid_edge, is_valid_node
Comment thread
amanchoudhri marked this conversation as resolved.
Outdated
from .struct import BELGraph, Pipeline, Query
from .struct.operations import union
from .version import get_version
152 changes: 152 additions & 0 deletions src/pybel/io/mongodb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# -*- coding: utf-8 -*-
"""An exporter from PyBEL graphs to a local Mongo database."""

import copy
from typing import Any, List, Mapping, Tuple

import pymongo
from pymongo.collection import Collection

import pybel
from pybel.schema import is_valid_edge, is_valid_node

__all__ = [
'_entity_to_dict',
'to_mongodb',
'find_nodes',
'get_edges_from_node',
'get_edges_from_criteria'
]

client = pymongo.MongoClient()


def _handle_member(elem):
"""Convert a member element from a complex or composite to a dictionary."""
# Convert each member element to a dictionary
new_elem = dict(elem)
# Remove the 'bel' and 'id' properties (which occur seemingly at random)
for prop in ('bel', 'id'):
if prop in new_elem.keys():
del new_elem[prop]
return new_elem


def _entity_to_dict(entity: pybel.dsl.Entity) -> Mapping[str, Any]:
"""Input a pybel Entity and return a dict representing it."""
new_node = dict(entity)
if new_node['function'] in ['Complex', 'Composite']:
Comment thread
amanchoudhri marked this conversation as resolved.
Outdated
new_node['members'] = list(map(_handle_member, new_node['members']))
return new_node


def to_mongodb(graph: pybel.BELGraph, db_name: str, collection_name: str) -> Collection:
"""Export the given BELGraph to a MongoDB.

In order to use this function, MongoDB must already be running locally.

:param graph: the graph to be exported
:param db_name: the name of the MongoDB to which the graph should be exported
:param collection_name: the name of the collection within the MongoDB where the graph will be stored.
:return: the collection that now stores the graph
"""
# Access (or create) the specified database and collection
db = client[db_name]
Comment thread
amanchoudhri marked this conversation as resolved.
collection = db[collection_name]
# If a collection with the same name already exists, drop its contents
Comment thread
amanchoudhri marked this conversation as resolved.
collection.drop()

# Add the nodes
for node in graph:
if not is_valid_node(node):
# TODO: Raise/log on invalid node
pass
# Add a 'type' parameter to avoid confusing nodes and links
n = copy.deepcopy(node)
n = _entity_to_dict(n)
n['type'] = 'node'
collection.insert_one(n)
Comment thread
amanchoudhri marked this conversation as resolved.
Outdated

# Add the edges
edges = pybel.to_nodelink(graph)['links']
for edge in edges:
if not is_valid_edge(edge):
# TODO: Raise/log on invalid edge
pass
# Add a 'type' parameter to avoid confusing nodes and links
e = copy.deepcopy(edge)
e['type'] = 'link'
collection.insert_one(e)

return collection


def find_nodes(
collection: Collection,
name: str = None,
identifier: str = None,
variants: List[pybel.dsl.EntityVariant] = None,
Comment thread
amanchoudhri marked this conversation as resolved.
Outdated
) -> List[Mapping[str, Any]]:
"""Find all the nodes that match the given criteria from a MongoDB Collection where a graph is stored.

:param collection: A MongoDB collection within a database where a PyBEL graph has been stored
:param name: The name of the desired node
:param identifier: The identifier of the desired node
:param variants: A list of variants that the desired node should contain. Note: nodes that contain the variants in addition to specified variants will be matched.
:return: A list containing all the nodes that match the given criteria
"""
if not (name or identifier):
Comment thread
amanchoudhri marked this conversation as resolved.
Outdated
raise ValueError("Either a 'name' or 'identifier' is required to find a node.")
filter_ = {'type': 'node'}
if name:
filter_['concept.name'] = name
if identifier:
filter_['concept.identifier'] = identifier
if variants:
filter_['variants'] = variants

return list(collection.find(filter_))
Comment thread
amanchoudhri marked this conversation as resolved.
Outdated


def get_edges_from_node(collection: Collection, node: Mapping[str, Any]) -> List[Mapping[str, Any]]:
"""Return all the edges for the given node.

:param collection: A MongoDB collection within a database where a PyBEL graph has been stored
:param node: The node whose edges should be returned
:return: A list of the edges pointing to or from the given node
"""
if not is_valid_node(node):
raise ValueError("Invalid node ", node)
# Remove the _id and type properties from the node (since they won't be included in the edge source/target information)
n = copy.deepcopy(node)
# Convert the pybel node to a dict so it can be matched against entries in the MongoDB
n = _entity_to_dict(n)
del n['_id'], n['type']
# Find all the links where either the source or the target is node n
filter_ = {'type': 'link', '$or': [{'source': n}, {'target': n}]}
return list(collection.find(filter_))


def get_edges_from_criteria(
collection: Collection,
node_name: str = None,
node_identifier: str = None,
node_variants: List[pybel.dsl.EntityVariant] = None,
) -> List[Tuple[Mapping[str, Any], List[Mapping[str, Any]]]]:
"""Get all the edges for nodes that match the given criteria and return in a list of tuples.

:param collection: A MongoDB collection within a database where a PyBEL graph has been stored
:param name: The name of the desired node
:param identifier: The identifier of the desired node
:param variants: A list of variants that the desired node should contain. Note: nodes that contain the variants in addition to specified variants will be matched.
:return: A list of tuples. The first element of each tuple is the node, and the second element is a list of the edges.
"""
if not (node_name or node_identifier):
raise ValueError("Either a name or an identifier is required to get edges.")
matching_nodes = find_nodes(collection, name=node_name, identifier=node_identifier, variants=node_variants)
edges = []
Comment thread
amanchoudhri marked this conversation as resolved.
Outdated
for node in matching_nodes:
matching_edges = get_edges_from_node(collection, node)
# Append (node, [edge1, edge2...]) to edges
edges.append((node, matching_edges))
return edges
120 changes: 120 additions & 0 deletions tests/test_io/test_mongodb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-

"""Tests for the jsonschema node validation."""

import copy
from typing import Any, List, Mapping, Tuple
import unittest

import pymongo
from pymongo.collection import Collection

from pybel import BELGraph, to_nodelink
from pybel.dsl import Entity, ComplexAbundance, Gene, Protein, Rna
from pybel.io.mongodb import (
_entity_to_dict,
to_mongodb,
find_nodes,
get_edges_from_node,
get_edges_from_criteria
)
from pybel.testing.utils import n

g1 = Gene('hgnc', '1')
r1 = Rna('hgnc', '1')
p1 = Protein('hgnc', '1')
g2 = Gene('hgnc', '2')
r2 = Rna('hgnc', '2')
p2 = Protein('hgnc', '2')
g3 = Gene('hgnc', '3')
p3 = Protein('hgnc', '3')

ca = ComplexAbundance([p1, g2], name='ca', namespace='hgnc')

client = pymongo.MongoClient()
TEST_DB = client['test_db']
TEST_COLLECTION = TEST_DB['test_collection']
TEST_COLLECTION.drop()


class TestMongoDB(unittest.TestCase):
"""Tests for the MongoDB exporting and querying."""
def setUp(self):
"""Create and export a test graph."""
Comment thread
amanchoudhri marked this conversation as resolved.
Outdated
self.graph = BELGraph()
self.graph.add_increases(ca, r2, citation=n(), evidence=n())
self.graph.add_increases(p2, p3, citation=n(), evidence=n())
self.graph.add_decreases(p3, p1, citation=n(), evidence=n())
self.links = to_nodelink(self.graph)['links']
self.collection = to_mongodb(self.graph, TEST_DB.name, TEST_COLLECTION.name)

def assert_node(self, node: Mapping[str, Any], assert_in=True) -> None:
"""Assert that the given node is present or not present in the MongoDB collection."""
assert_fn = self.assertEqual
if not assert_in:
assert_fn = self.assertNotEqual
# Convert the node to a dict so it can be matched against entries in the MongoDB
node_dict = _entity_to_dict(node)
match = self.collection.find_one(node_dict)
# If the passed node has a MongoDB unique _id, check that the two ids match
if '_id' in node.keys():
assert_fn(node_dict['_id'], match['_id'])
else:
# Otherwise, delete the parameters type and _id unique to the to_mongodb output
if match is not None:
del match['_id'], match['type']
# And check that the two nodes are equal
assert_fn(node_dict, match)

def assert_edge(self, edge: dict, assert_in=True) -> None:
"""Assert that the given edge is or isn't present in the Mongo collection."""
assert_fn = self.assertEqual
if not assert_in:
assert_fn = self.assertNotEqual
if type(edge) is not dict:
Comment thread
amanchoudhri marked this conversation as resolved.
Outdated
raise ValueError('Expeced type(edge) to be dict. Insead found: ', type(edge))
# Query the collection for the given edge
match = self.collection.find_one(edge)
if match is not None:
# If a match was found, delete the _id and type keys
del match['_id'], match['type']
# Assert that the given edge and match are or are not equal (based on assert_in param)
assert_fn(edge, match)

def test_export(self):
"""Test that the to_mongodb export function operates as expeced."""
# Assert that all nodes in self.graph are in self.collection
for node in self.graph:
self.assert_node(node)
# Assert that the two nodes not in self.graph are not in self.collection
self.assert_node(g1, assert_in=False)
self.assert_node(r1, assert_in=False)
# Assert that every edge from self.graph is in self.collection
for link in self.links:
self.assert_edge(link)

def test_query_nodes(self):
"""Test that the find_nodes() function correctly finds the desired nodes."""
for node in self.graph:
# Convert the node to a dict
n = _entity_to_dict(node)
# Get the concept entry (where the name / identifier are stored)
concept = n['concept']
name, identifier, variants = None, None, None
# Query based on name, identifier, variants if they are present
if 'name' in concept.keys():
name = concept['name']
if 'identifier' in concept.keys():
identifier = concept['identifier']
if 'variants' in n.keys():
variants = n['variants']

matches = find_nodes(self.collection, name=name, identifier=identifier, variants=variants)
for match in matches:
del match['_id'], match['type']

self.assertIn(n, matches)


if __name__ == '__main__':
unittest.main()