Skip to content
This repository has been archived by the owner on Feb 23, 2022. It is now read-only.

Commit

Permalink
Merge pull request #253 from multinet-app/auto_detect_node_tables
Browse files Browse the repository at this point in the history
Automatically detect node tables in graph creation
  • Loading branch information
jjnesbitt committed Jan 22, 2020
2 parents 33c05d9 + db435cb commit 0526ee1
Show file tree
Hide file tree
Showing 11 changed files with 110 additions and 90 deletions.
2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"axios": "^0.18.1",
"core-js": "^2.6.5",
"material-design-icons-iconfont": "^5.0.1",
"multinet": "0.6.0",
"multinet": "0.7.0",
"vue": "^2.6.10",
"vue-router": "^3.0.2",
"vuetify": "^2.1.5"
Expand Down
21 changes: 1 addition & 20 deletions client/src/components/GraphDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,6 @@
</v-flex>
</v-layout>

<v-layout wrap>
<v-flex>
<v-select
filled
chips
class="choose-tables"
clearable
deletable-chips
label="Choose node tables"
multiple
v-model="graphNodeTables"
:items="nodeTables"
/>
</v-flex>
</v-layout>

<v-layout wrap>
<v-flex>
<v-select
Expand Down Expand Up @@ -89,7 +73,6 @@ import api from '@/api';
export default Vue.extend({
name: 'GraphDialog',
props: {
nodeTables: Array,
edgeTables: Array,
workspace: String,
},
Expand All @@ -98,13 +81,12 @@ export default Vue.extend({
graphCreationErrors: [] as string[],
graphDialog: false,
graphEdgeTable: null as string | null,
graphNodeTables: [] as string[],
newGraph: '',
};
},
computed: {
graphCreateDisabled(): boolean {
return this.graphNodeTables.length === 0 || !this.graphEdgeTable || !this.newGraph;
return !this.graphEdgeTable || !this.newGraph;
},
},
Expand All @@ -117,7 +99,6 @@ export default Vue.extend({
}
const response = await api.createGraph(workspace, newGraph, {
nodeTables: this.graphNodeTables,
edgeTable: this.graphEdgeTable,
});
Expand Down
8 changes: 4 additions & 4 deletions client/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5517,10 +5517,10 @@ multimatch@^2.1.0:
arrify "^1.0.0"
minimatch "^3.0.0"

multinet@0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/multinet/-/multinet-0.6.0.tgz#adad62084f24a469da71fc17f812326bd147dc54"
integrity sha512-hALbdK2fuH5OWf9+1BfRrE5OL9PCh3q6P+E1f6b5Nwnfi0ftHvWtZAYr+4xnjpEyl/x7ShBe/8+jI0xuHkJGDQ==
multinet@0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/multinet/-/multinet-0.7.0.tgz#9688d20dd0d22d96b9d32631f4734d667c6701d6"
integrity sha512-kMNlS+VU4wTv0nzQ0cAUhygPpCOEYHdchbMvuplCvtq7xbX97U+TiH8ugStIdrPpF7R+hKmchIGHroYJ4THQxg==
dependencies:
axios "^0.19.0"

Expand Down
72 changes: 22 additions & 50 deletions multinet/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from webargs import fields
from webargs.flaskparser import use_kwargs

from typing import Any, Optional, List, Dict, Set
from typing import Any, Optional
from .types import EdgeDirection, TableType

from . import db, util
Expand Down Expand Up @@ -137,72 +137,44 @@ def delete_workspace(workspace: str) -> Any:


@bp.route("/workspaces/<workspace>/graphs/<graph>", methods=["POST"])
@use_kwargs({"node_tables": fields.List(fields.Str()), "edge_table": fields.Str()})
@use_kwargs({"edge_table": fields.Str()})
@swag_from("swagger/create_graph.yaml")
def create_graph(
workspace: str,
graph: str,
node_tables: Optional[List[str]] = None,
edge_table: Optional[str] = None,
) -> Any:
def create_graph(workspace: str, graph: str, edge_table: Optional[str] = None) -> Any:
"""Create a graph."""

if not node_tables or not edge_table:
body = request.data.decode("utf8")
raise MalformedRequestBody(body)

missing = [
arg[0]
for arg in [("node_tables", node_tables), ("edge_table", edge_table)]
if arg[1] is None
]
if missing:
raise RequiredParamsMissing(missing)
if not edge_table:
raise RequiredParamsMissing(["edge_table"])

loaded_workspace = db.db(workspace)
if loaded_workspace.has_graph(graph):
raise AlreadyExists("Graph", graph)

existing_tables = set([x["name"] for x in loaded_workspace.collections()])
edges = loaded_workspace.collection(edge_table).all()
# Get reference tables with respective referenced keys,
# tables in the _from column and tables in the _to column
edge_table_properties = util.get_edge_table_properties(workspace, edge_table)
referenced_tables = edge_table_properties["table_keys"]
from_tables = edge_table_properties["from_tables"]
to_tables = edge_table_properties["to_tables"]

# Iterate through each edge and check for undefined tables
errors = []
valid_tables: Dict[str, Set[str]] = dict()
invalid_tables = set()
for edge in edges:
nodes = (edge["_from"].split("/"), edge["_to"].split("/"))

for (table, key) in nodes:
if table not in existing_tables:
invalid_tables.add(table)
elif table in valid_tables:
valid_tables[table].add(key)
else:
valid_tables[table] = {key}

if invalid_tables:
for table in invalid_tables:
for table, keys in referenced_tables.items():
if not loaded_workspace.has_collection(table):
errors.append(f"Reference to undefined table: {table}")
else:
table_keys = set(loaded_workspace.collection(table).keys())
undefined = keys - table_keys

# Iterate through each node table and check for nonexistent keys
for table in valid_tables:
existing_keys = set(
[x["_key"] for x in loaded_workspace.collection(table).all()]
)
nonexistent_keys = valid_tables[table] - existing_keys

if len(nonexistent_keys) > 0:
errors.append(
f"Nonexistent keys {', '.join(nonexistent_keys)} "
f"referenced in table: {table}"
)
if undefined:
errors.append(
f"Nonexistent keys {', '.join(undefined)} "
f"referenced in table: {table}"
)

# TODO: Update this with the proper JSON schema
if errors:
raise ValidationFailed(errors)

db.create_graph(workspace, graph, node_tables, edge_table)
db.create_graph(workspace, graph, edge_table, from_tables, to_tables)
return graph


Expand Down
29 changes: 19 additions & 10 deletions multinet/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@

from arango import ArangoClient
from arango.database import StandardDatabase, StandardCollection
from arango.exceptions import DatabaseCreateError
from arango.exceptions import DatabaseCreateError, EdgeDefinitionCreateError
from requests.exceptions import ConnectionError

from typing import Callable, Any, Optional, Sequence, List, Generator, Tuple
from typing import Callable, Any, Optional, Sequence, List, Set, Generator, Tuple
from mypy_extensions import TypedDict
from .types import EdgeDirection, TableType

Expand All @@ -18,6 +18,7 @@
NodeNotFound,
InvalidName,
AlreadyExists,
GraphCreationError,
)


Expand Down Expand Up @@ -316,23 +317,31 @@ def aql_query(
def create_graph(
workspace: str,
graph: str,
node_tables: List[str],
edge_table: str,
from_vertex_collections: Set[str],
to_vertex_collections: Set[str],
arango: ArangoClient,
) -> bool:
"""Create a graph named `graph`, defined by`node_tables` and `edge_table`."""
space = db(workspace, arango=arango)
if space.has_graph(graph):
return False
else:
g = space.create_graph(graph)
g.create_edge_definition(
edge_collection=edge_table,
from_vertex_collections=node_tables,
to_vertex_collections=node_tables,

try:
space.create_graph(
graph,
edge_definitions=[
{
"edge_collection": edge_table,
"from_vertex_collections": list(from_vertex_collections),
"to_vertex_collections": list(to_vertex_collections),
}
],
)
except EdgeDefinitionCreateError as e:
raise GraphCreationError(str(e))

return True
return True


@with_client
Expand Down
12 changes: 12 additions & 0 deletions multinet/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,15 @@ def __init__(self, error: str):
def flask_response(self) -> FlaskTuple:
"""Generate a 400 error."""
return (self.error, "400 Decode Failed")


class GraphCreationError(ServerError):
"""Exception for errors when creating a graph in Arango."""

def __init__(self, message: str):
"""Initialize error message."""
self.message = message

def flask_response(self) -> FlaskTuple:
"""Generate a 500 error."""
return (self.message, "500 Graph Creation Failed")
16 changes: 15 additions & 1 deletion multinet/types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
"""Custom types for Multinet codebase."""
from typing_extensions import Literal
from typing import Dict, Set
from typing_extensions import Literal, TypedDict


EdgeDirection = Literal["all", "incoming", "outgoing"]
TableType = Literal["all", "node", "edge"]


class EdgeTableProperties(TypedDict):
"""Describes gathered information about an edge table."""

# Dictionary mapping all referenced tables to the referenced keys within that table
table_keys: Dict[str, Set[str]]

# Keeps track of which tables are referenced in the _from column
from_tables: Set[str]

# Keeps track of which tables are referenced in the _to column
to_tables: Set[str]
35 changes: 34 additions & 1 deletion multinet/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,47 @@

from flask import Response

from typing import Sequence, Any, Generator
from typing import Sequence, Any, Generator, Dict, Set

from . import db
from .errors import DatabaseNotLive, DecodeFailed
from .types import EdgeTableProperties

TEST_DATA_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../test/data"))


def get_edge_table_properties(workspace: str, edge_table: str) -> EdgeTableProperties:
"""
Return extracted information about an edge table.
Extracts 3 pieces of data from an edge table.
table_keys: A mapping of all referenced tables to their respective referenced keys.
from_tables: A set containing the tables referenced in the _from column.
to_tables: A set containing the tables referenced in the _to column.
"""

loaded_workspace = db.db(workspace)
edges = loaded_workspace.collection(edge_table).all()

tables_to_keys: Dict[str, Set[str]] = {}
from_tables = set()
to_tables = set()

for edge in edges:
from_node, to_node = edge["_from"].split("/"), edge["_to"].split("/")
from_tables.add(from_node[0])
to_tables.add(to_node[0])

for table, key in (from_node, to_node):
if table in tables_to_keys:
tables_to_keys[table].add(key)
else:
tables_to_keys[table] = {key}

return dict(table_keys=tables_to_keys, from_tables=from_tables, to_tables=to_tables)


def generate(iterator: Sequence[Any]) -> Generator[str, None, None]:
"""Return a generator that yields an iterator's contents into a JSON list."""
yield "["
Expand Down
2 changes: 1 addition & 1 deletion multinetjs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "multinet",
"version": "0.6.0",
"version": "0.7.0",
"description": "Multinet client library",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
2 changes: 0 additions & 2 deletions multinetjs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ export interface UploadTableOptionsSpec {
}

export interface CreateGraphOptionsSpec {
nodeTables: string[];
edgeTable: string;
}

Expand Down Expand Up @@ -151,7 +150,6 @@ class MultinetAPI {

public createGraph(workspace: string, graph: string, options: CreateGraphOptionsSpec): Promise<string> {
return this.client.post(`/workspaces/${workspace}/graphs/${graph}`, {
node_tables: options.nodeTables,
edge_table: options.edgeTable,
});
}
Expand Down
1 change: 1 addition & 0 deletions mypy_stubs/arango/exceptions.pyi
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
class DatabaseCreateError(Exception): ...
class EdgeDefinitionCreateError(Exception): ...

0 comments on commit 0526ee1

Please sign in to comment.