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 #341 from multinet-app/workspace_names
Browse files Browse the repository at this point in the history
Fix workspace names
  • Loading branch information
jjnesbitt committed Mar 12, 2020
2 parents bfbfaab + b41b997 commit 9be1de9
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 29 deletions.
3 changes: 3 additions & 0 deletions multinet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from typing import Optional, MutableMapping, Any, Tuple, Union

from multinet import api
from multinet.db import register_legacy_workspaces
from multinet import uploaders, downloaders
from multinet.errors import ServerError

Expand All @@ -36,6 +37,8 @@ def create_app(config: Optional[MutableMapping] = None) -> Flask:
app.register_blueprint(downloaders.csv.bp, url_prefix="/api")
app.register_blueprint(downloaders.d3_json.bp, url_prefix="/api")

register_legacy_workspaces()

# Register error handler.
@app.errorhandler(ServerError)
def handle_error(error: ServerError) -> Tuple[Any, Union[int, str]]:
Expand Down
11 changes: 10 additions & 1 deletion multinet/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,15 @@ def delete_workspace(workspace: str) -> Any:
return workspace


@bp.route("/workspaces/<workspace>/name", methods=["PUT"])
@use_kwargs({"name": fields.Str()})
@swag_from("swagger/rename_workspace.yaml")
def rename_workspace(workspace: str, name: str) -> Any:
"""Delete a workspace."""
db.rename_workspace(workspace, name)
return name


@bp.route("/workspaces/<workspace>/graphs/<graph>", methods=["POST"])
@use_kwargs({"edge_table": fields.Str()})
@swag_from("swagger/create_graph.yaml")
Expand All @@ -146,7 +155,7 @@ def create_graph(workspace: str, graph: str, edge_table: Optional[str] = None) -
if not edge_table:
raise RequiredParamsMissing(["edge_table"])

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

Expand Down
120 changes: 98 additions & 22 deletions multinet/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@
from arango.exceptions import DatabaseCreateError, EdgeDefinitionCreateError
from requests.exceptions import ConnectionError

from typing import Any, Sequence, List, Set, Generator, Tuple, Dict
from typing import Any, Sequence, List, Dict, Set, Generator, Tuple, Union
from typing_extensions import TypedDict
from multinet.types import EdgeDirection, TableType
from multinet.errors import InternalServerError
from multinet.util import generate_arango_workspace_name

from multinet.errors import (
BadQueryArgument,
WorkspaceNotFound,
TableNotFound,
GraphNotFound,
NodeNotFound,
InvalidName,
AlreadyExists,
GraphCreationError,
)
Expand Down Expand Up @@ -58,38 +58,114 @@ def check_db() -> bool:
return False


def register_legacy_workspaces() -> None:
"""Add legacy workspaces to the workspace mapping."""
sysdb = db("_system")
coll = workspace_mapping_collection()

databases = {name for name in sysdb.databases() if name != "_system"}
registered = {doc["internal"] for doc in workspace_mapping_collection().all()}

unregistered = databases - registered
for workspace in unregistered:
coll.insert({"name": workspace, "internal": workspace})


def workspace_mapping_collection() -> StandardCollection:
"""Return the collection used for mapping external to internal workspace names."""
sysdb = db("_system")

if not sysdb.has_collection("workspace_mapping"):
sysdb.create_collection("workspace_mapping")

return sysdb.collection("workspace_mapping")


def workspace_mapping(name: str) -> Union[Dict[str, str], None]:
"""
Get the document containing the workspace mapping for :name: (if it exists).
Returns the document if found, otherwise returns None.
"""
coll = workspace_mapping_collection()
docs = list(coll.find({"name": name}, limit=1))

if docs:
return docs[0]

return None


def workspace_exists(name: str) -> bool:
"""Convinience wrapper for checking if a workspace exists."""
return bool(workspace_mapping(name))


def workspace_exists_internal(name: str) -> bool:
"""Return True if a workspace with the internal name :name: exists."""
sysdb = db("_system")
return sysdb.has_database(name)


def create_workspace(name: str) -> None:
"""Create a new workspace named `name`."""
sysdb = db("_system")
if not sysdb.has_database(name):
try:
sysdb.create_database(name)
except DatabaseCreateError:
raise InvalidName(name)
else:

if workspace_exists(name):
raise AlreadyExists("Workspace", name)

coll = workspace_mapping_collection()
new_doc = {"name": name, "internal": generate_arango_workspace_name()}
coll.insert(new_doc)

try:
db("_system").create_database(new_doc["internal"])
except DatabaseCreateError:
# Could only happen if there's a name collisison
raise InternalServerError()


def rename_workspace(old_name: str, new_name: str) -> None:
"""Rename a workspace."""
doc = workspace_mapping(old_name)
if not doc:
raise WorkspaceNotFound(old_name)

if workspace_exists(new_name):
raise AlreadyExists("Workspace", new_name)

doc["name"] = new_name
coll = workspace_mapping_collection()
coll.update(doc)


def delete_workspace(name: str) -> None:
"""Delete the workspace named `name`."""
doc = workspace_mapping(name)
if not doc:
raise WorkspaceNotFound(name)

sysdb = db("_system")
if sysdb.has_database(name):
sysdb.delete_database(name)
coll = workspace_mapping_collection()

sysdb.delete_database(doc["internal"])
coll.delete(doc["_id"])


def get_workspace(name: str) -> WorkspaceSpec:
"""Return a single workspace, if it exists."""
sysdb = db("_system")
if not sysdb.has_database(name):
if not workspace_exists(name):
raise WorkspaceNotFound(name)

return {"name": name, "owner": "", "readers": [], "writers": []}


def get_workspace_db(name: str) -> StandardDatabase:
"""Return the Arango database associated with a workspace, if it exists."""
get_workspace(name)
return db(name)
doc = workspace_mapping(name)
if not doc:
raise WorkspaceNotFound(name)

return db(doc["internal"])


def get_graph_collection(workspace: str, graph: str) -> Graph:
Expand All @@ -112,8 +188,8 @@ def get_table_collection(workspace: str, table: str) -> StandardCollection:

def get_workspaces() -> Generator[str, None, None]:
"""Return a list of all workspace names."""
sysdb = db("_system")
return (workspace for workspace in sysdb.databases() if workspace != "_system")
coll = workspace_mapping_collection()
return (doc["name"] for doc in coll.all())


def workspace_tables(
Expand Down Expand Up @@ -282,7 +358,7 @@ def graph_nodes(workspace: str, graph: str, offset: int, limit: int) -> GraphNod

def table_fields(workspace: str, table: str) -> List[str]:
"""Return a list of column names for `query.table` in `query.workspace`."""
space = db(workspace)
space = get_workspace_db(workspace)
if space.has_collection(table) and space.collection(table).count() > 0:
sample = space.collection(table).random()
return list(sample.keys())
Expand All @@ -292,7 +368,7 @@ def table_fields(workspace: str, table: str) -> List[str]:

def delete_table(workspace: str, table: str) -> str:
"""Delete a table."""
space = db(workspace)
space = get_workspace_db(workspace)
if space.has_collection(table):
space.delete_collection(table)

Expand All @@ -301,7 +377,7 @@ def delete_table(workspace: str, table: str) -> str:

def aql_query(workspace: str, query: str) -> Generator[Any, None, None]:
"""Perform an AQL query in the given workspace."""
aql = db(workspace).aql
aql = get_workspace_db(workspace).aql

cursor = aql.execute(query)
return cursor
Expand All @@ -315,7 +391,7 @@ def create_graph(
to_vertex_collections: Set[str],
) -> bool:
"""Create a graph named `graph`, defined by`node_tables` and `edge_table`."""
space = db(workspace)
space = get_workspace_db(workspace)
if space.has_graph(graph):
return False

Expand All @@ -338,7 +414,7 @@ def create_graph(

def delete_graph(workspace: str, graph: str) -> str:
"""Delete graph `graph` from workspace `workspace`."""
space = db(workspace)
space = get_workspace_db(workspace)
if space.has_graph(graph):
space.delete_graph(graph)

Expand Down
31 changes: 31 additions & 0 deletions multinet/swagger/rename_workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
Rename a workspace
---
parameters:
- $ref: "#/parameters/workspace"
- name: name
in: query
description: The new name of the workspace
required: true
schema:
type: string
example: Renamed Workspace
responses:
200:
description: Workspace successfully renamed
schema:
type: string
example: workspace3

404:
description: Workspace not found
schema:
type: string
example: workspace3

409:
description: Workspace already exists
schema:
type: string
example: workspace3
tags:
- workspace
2 changes: 1 addition & 1 deletion multinet/uploaders/csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def upload(workspace: str, table: str) -> Any:

# Set the collection, paying attention to whether the data contains
# _from/_to fields.
space = db.db(workspace)
space = db.get_workspace_db(workspace)
if space.has_collection(table):
coll = space.collection(table)
else:
Expand Down
2 changes: 1 addition & 1 deletion multinet/uploaders/d3_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def upload(workspace: str, graph: str) -> Any:
if len(errors) > 0:
raise ValidationFailed(errors)

space = db.db(workspace)
space = db.get_workspace_db(workspace)
if space.has_graph(graph):
raise AlreadyExists("graph", graph)

Expand Down
2 changes: 1 addition & 1 deletion multinet/uploaders/nested_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def upload(workspace: str, graph: str) -> Any:
# Set up the parameters.
data = request.data.decode("utf8")

space = db.db(workspace)
space = db.get_workspace_db(workspace)
if space.has_graph(graph):
raise AlreadyExists("graph", graph)

Expand Down
2 changes: 1 addition & 1 deletion multinet/uploaders/newick.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def upload(workspace: str, graph: str) -> Any:

validate_newick(tree)

space = db.db(workspace)
space = db.get_workspace_db(workspace)
if space.has_graph(graph):
raise AlreadyExists("graph", graph)

Expand Down
10 changes: 8 additions & 2 deletions multinet/util.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Utility functions."""
import json
import os
import json

from uuid import uuid1
from flask import Response
from typing import Any, Generator, Dict, Set, Iterable

Expand Down Expand Up @@ -35,7 +36,7 @@ def get_edge_table_properties(workspace: str, edge_table: str) -> EdgeTablePrope
to_tables: A set containing the tables referenced in the _to column.
"""

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

tables_to_keys: Dict[str, Set[str]] = {}
Expand Down Expand Up @@ -98,3 +99,8 @@ def data_path(file_name: str) -> str:
file_path = os.path.join(TEST_DATA_DIR, file_name)
print(file_path)
return file_path


def generate_arango_workspace_name() -> str:
"""Generate a string that can be used as an ArangoDB workspace name."""
return f"w-{uuid1()}"
22 changes: 22 additions & 0 deletions mypy_stubs/arango/collection.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class Collection:
) -> bool: ...
def keys(self) -> Cursor: ...
def all(self, skip: Optional[Any] = ..., limit: Optional[Any] = ...) -> Cursor: ...
def find(self, filters: Dict, skip: int = None, limit: int = None) -> Cursor: ...
def random(self) -> Dict: ...

class StandardCollection(Collection):
Expand All @@ -30,6 +31,27 @@ class StandardCollection(Collection):
overwrite: bool = ...,
return_old: bool = ...,
) -> List[Union[Dict, ArangoError]]: ...
def delete(
self,
document: Union[Dict, str],
rev: Optional[str] = ...,
check_rev: bool = ...,
ignore_missing: bool = ...,
return_old: bool = ...,
sync: Optional[Any] = ...,
silent: bool = ...,
) -> Union[Dict, bool]: ...
def update(
self,
document: Union[Dict, str],
check_rev: bool = ...,
merge: bool = ...,
keep_none: bool = ...,
return_new: bool = ...,
return_old: bool = ...,
sync: Optional[Any] = ...,
silent: bool = ...,
) -> Union[Dict, bool]: ...

class VertexCollection(Collection): ...
class EdgeCollection(Collection): ...

0 comments on commit 9be1de9

Please sign in to comment.