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 #153 from multinet-app/add_typing
Browse files Browse the repository at this point in the history
Add type hints
  • Loading branch information
waxlamp committed Aug 29, 2019
2 parents c9ecdfb + 44aec6f commit e59c4cb
Show file tree
Hide file tree
Showing 21 changed files with 325 additions and 145 deletions.
3 changes: 3 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ jobs:
- run:
name: Python lint tests
command: pipenv run lint
- run:
name: Python type checking
command: pipenv run typecheck
- save_cache:
key: pipenv-{{ checksum "Pipfile.lock" }}
paths:
Expand Down
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ DOC_SERVE_PORT=8000

FLASK_APP=multinet
FLASK_ENV=development

MYPYPATH=mypy_stubs
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ coverage.xml
*.cover
.hypothesis/
.pytest_cache/
.mypy_cache

# pyenv
.python-version
Expand Down
4 changes: 3 additions & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ verify_ssl = true

[packages]
# Core packages
multinet = {path = ".", editable = true}
multinet = {path = ".",editable = true}
python-arango = "==4.4.0"
newick = "==0.9.2"
uuid = "==1.30"
Expand All @@ -26,6 +26,7 @@ pytest-cov = "==2.7.1"
pytest-xdist = "==1.29.0"
recommonmark = "==0.5.0"
pre-commit = "==1.18.2"
flake8-mypy = "*"
pep8-naming = "*"

[requires]
Expand All @@ -40,5 +41,6 @@ build-docs = "make html -C docs"
serve-docs = "python -m http.server ${DOC_SERVE_PORT} -d docs/_build/html"
lint = "flake8"
test = "pytest -v -W ignore::DeprecationWarning test"
typecheck = "mypy -p multinet --disallow-untyped-defs"
coverage = "pytest -W ignore::DeprecationWarning test --cov=multinet"
format = "black ."
61 changes: 60 additions & 1 deletion Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions multinet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
from flask.logging import default_handler
from flask_cors import CORS

from typing import Optional, MutableMapping, Any, Tuple, Union

from . import api
from . import uploaders
from .errors import ServerError


def create_app(config=None):
def create_app(config: Optional[MutableMapping] = None) -> Flask:
"""Create a Multinet app instance."""
app = Flask(__name__)
CORS(app)
Expand All @@ -24,11 +26,11 @@ def create_app(config=None):

# Register error handler.
@app.errorhandler(ServerError)
def handle_error(error):
def handle_error(error: ServerError) -> Tuple[Any, Union[int, str]]:
return error.flask_response()

@app.route("/")
def about():
def about() -> str:
return """
<h1>Multinet API</h1>
<div>
Expand Down
52 changes: 37 additions & 15 deletions multinet/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from webargs import fields
from webargs.flaskparser import use_kwargs

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

from . import db, util
from .errors import (
ValidationFailed,
Expand All @@ -17,49 +20,51 @@


@bp.route("/workspaces", methods=["GET"])
def get_workspaces():
def get_workspaces() -> Any:
"""Retrieve list of workspaces."""
return util.stream(db.get_workspaces())


@bp.route("/workspaces/<workspace>", methods=["GET"])
def get_workspace(workspace):
def get_workspace(workspace: str) -> Any:
"""Retrieve a single workspace."""
return db.get_workspace(workspace)


@bp.route("/workspaces/<workspace>/tables", methods=["GET"])
@use_kwargs({"type": fields.Str()})
def get_workspace_tables(workspace, type="all"):
def get_workspace_tables(workspace: str, type: TableType = "all") -> Any:
"""Retrieve the tables of a single workspace."""
tables = db.workspace_tables(workspace, type)
return util.stream(tables)


@bp.route("/workspaces/<workspace>/tables/<table>", methods=["GET"])
@use_kwargs({"offset": fields.Int(), "limit": fields.Int()})
def get_table_rows(workspace, table, offset=0, limit=30):
def get_table_rows(workspace: str, table: str, offset: int = 0, limit: int = 30) -> Any:
"""Retrieve the rows and headers of a table."""
rows = db.workspace_table(workspace, table, offset, limit)
return util.stream(rows)


@bp.route("/workspaces/<workspace>/graphs", methods=["GET"])
def get_workspace_graphs(workspace):
def get_workspace_graphs(workspace: str) -> Any:
"""Retrieve the graphs of a single workspace."""
graphs = db.workspace_graphs(workspace)
return util.stream(graphs)


@bp.route("/workspaces/<workspace>/graphs/<graph>", methods=["GET"])
def get_workspace_graph(workspace, graph):
def get_workspace_graph(workspace: str, graph: str) -> Any:
"""Retrieve information about a graph."""
return db.workspace_graph(workspace, graph)


@bp.route("/workspaces/<workspace>/graphs/<graph>/nodes", methods=["GET"])
@use_kwargs({"offset": fields.Int(), "limit": fields.Int()})
def get_graph_nodes(workspace, graph, offset=0, limit=30):
def get_graph_nodes(
workspace: str, graph: str, offset: int = 0, limit: int = 30
) -> Any:
"""Retrieve the nodes of a graph."""
return db.graph_nodes(workspace, graph, offset, limit)

Expand All @@ -68,7 +73,7 @@ def get_graph_nodes(workspace, graph, offset=0, limit=30):
"/workspaces/<workspace>/graphs/<graph>/nodes/<table>/<node>/attributes",
methods=["GET"],
)
def get_node_data(workspace, graph, table, node):
def get_node_data(workspace: str, graph: str, table: str, node: str) -> Any:
"""Return the attributes associated with a node."""
return db.graph_node(workspace, graph, table, node)

Expand All @@ -77,7 +82,15 @@ def get_node_data(workspace, graph, table, node):
"/workspaces/<workspace>/graphs/<graph>/nodes/<table>/<node>/edges", methods=["GET"]
)
@use_kwargs({"direction": fields.Str(), "offset": fields.Int(), "limit": fields.Int()})
def get_graph_node(workspace, graph, table, node, direction="all", offset=0, limit=30):
def get_graph_node(
workspace: str,
graph: str,
table: str,
node: str,
direction: EdgeDirection = "all",
offset: int = 0,
limit: int = 30,
) -> Any:
"""Return the edges connected to a node."""
allowed = ["incoming", "outgoing", "all"]
if direction not in allowed:
Expand All @@ -87,14 +100,14 @@ def get_graph_node(workspace, graph, table, node, direction="all", offset=0, lim


@bp.route("/workspaces/<workspace>", methods=["POST"])
def create_workspace(workspace):
def create_workspace(workspace: str) -> Any:
"""Create a new workspace."""
db.create_workspace(workspace)
return workspace


@bp.route("/workspaces/<workspace>/aql", methods=["POST"])
def aql(workspace):
def aql(workspace: str) -> Any:
"""Perform an AQL query in the given workspace."""
query = request.data.decode("utf8")
if not query:
Expand All @@ -105,22 +118,31 @@ def aql(workspace):


@bp.route("/workspaces/<workspace>", methods=["DELETE"])
def delete_workspace(workspace):
def delete_workspace(workspace: str) -> Any:
"""Delete a workspace."""
db.delete_workspace(workspace)
return workspace


@bp.route("/workspaces/<workspace>/graph/<graph>", methods=["POST"])
@use_kwargs({"node_tables": fields.List(fields.Str()), "edge_table": fields.Str()})
def create_graph(workspace, graph, node_tables=None, edge_table=None):
def create_graph(
workspace: str,
graph: str,
node_tables: Optional[List[str]] = None,
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 for arg in [node_tables, edge_table] if arg is None]
missing = [
arg[0]
for arg in [("node_tables", node_tables), ("edge_table", edge_table)]
if arg[1] is None
]
if missing:
raise RequiredParamsMissing(missing)

Expand All @@ -133,7 +155,7 @@ def create_graph(workspace, graph, node_tables=None, edge_table=None):

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

0 comments on commit e59c4cb

Please sign in to comment.