Skip to content

Commit

Permalink
Add docstrings, fix code style.
Browse files Browse the repository at this point in the history
  • Loading branch information
chovanecm committed Jun 25, 2017
1 parent a504be9 commit dc4e87e
Show file tree
Hide file tree
Showing 10 changed files with 103 additions and 13 deletions.
12 changes: 9 additions & 3 deletions sacredboard/app/data/datastorage.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
"""Interfaces for data storage backend."""
"""Interfaces for data storage."""


class Cursor:
"""Interface that abstracts the cursor object returned from databases."""

def __init__(self):
"""Declare a new cursor to iterate over runs."""
pass

def count(self):
"""Return the number of items in this cursor."""
raise NotImplemented()

def __iter__(self):
"""Iterate over elements."""
raise NotImplemented()


class DataStorage:
"""
Interface for data backends. Defines the API for various data stores --- databases, file stores, etc. --- that
sacred supports.
Interface for data backends.
Defines the API for various data stores
databases, file stores, etc. --- that sacred supports.
"""

def __init__(self):
"""Initialize data accessor."""
pass

def get_run(self, run_id):
Expand Down
19 changes: 13 additions & 6 deletions sacredboard/app/data/filestorage.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,33 +49,36 @@ class FileStoreCursor(Cursor):
"""Implements the cursor for file stores."""

def __init__(self, count, iterable):
"""Initialize FileStoreCursor with a given iterable."""
self.iterable = iterable
self._count = count

def count(self):
"""
Return the number of runs in this query.
:return: int
"""
return self._count

def __iter__(self):
"""Iterate over runs."""
return iter(self.iterable)


class FileStorage(DataStorage):
"""Object to interface with one of sacred's file stores."""

def __init__(self, path_to_dir):
"""Initialize file storage run accessor."""
super().__init__()
self.path_to_dir = os.path.expanduser(path_to_dir)

def get_run(self, run_id):
"""
Return the run associated with a particular `run_id`.
:param run_id:
:param run_id:
:return: dict
:raises FileNotFoundError
"""
Expand All @@ -87,8 +90,10 @@ def get_run(self, run_id):
def get_runs(self, sort_by=None, sort_direction=None,
start=0, limit=None, query={"type": "and", "filters": []}):
"""
Return all runs in the file store. If a run is corrupt -- e.g. missing files --- it is skipped.
Return all runs in the file store.
If a run is corrupt, e.g. missing files, it is skipped.
:param sort_by: NotImplemented
:param sort_direction: NotImplemented
:param start: NotImplemented
Expand All @@ -106,7 +111,9 @@ def run_iterator():
try:
yield self.get_run(id)
except FileNotFoundError:
# An incomplete experiment is a corrupt experiment. Skip it for now.
# An incomplete experiment is a corrupt experiment.
# Skip it for now.
# TODO
pass

count = len(all_run_ids)
Expand Down
4 changes: 3 additions & 1 deletion sacredboard/app/data/mongodb.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ class MongoDbCursor(Cursor):
"""Implements Cursor for mongodb."""

def __init__(self, mongodb_cursor):
"""Initialize a MongoDB cursor."""
self.mongodb_cursor = mongodb_cursor

def count(self):
"""Returns the number of items in this cursor."""
"""Return the number of items in this cursor."""
return self.mongodb_cursor.count()

def __iter__(self):
"""Iterate over runs."""
return self.mongodb_cursor


Expand Down
1 change: 1 addition & 0 deletions sacredboard/app/process/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Sacredboard module for launching external processes."""
46 changes: 44 additions & 2 deletions sacredboard/app/process/process.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# coding=utf-8
"""Module for launching processes and reading their output."""
import atexit
import os
import select
Expand All @@ -7,9 +8,18 @@


class Process:
"""A process that can be run and read output from."""

instances = [] # type: List[Process]
"""Instances of all processes."""

def __init__(self, command):
"""
Define a new process but do not start it.
:param command: A command to start. Parameters separated with spaces
or as a list, e.g. "command arg1 arg2" or ["command", "arg1", "arg2"].
"""
if type(command == list):
self.command = command
else:
Expand All @@ -18,6 +28,7 @@ def __init__(self, command):
self.proc = None # type: Popen

def run(self):
"""Run the process."""
environment = os.environ.copy()
# necessary for reading from processes that don't flush
# stdout automatically
Expand All @@ -26,9 +37,15 @@ def run(self):
Process.instances.append(self)

def is_running(self):
"""Test if the process is running."""
return self.proc is not None and not bool(self.proc.poll())

def read_line(self, time_limit=None):
"""
Read a line from the process.
Block or wait for time_limit secs. Timeout does not work on Windows.
"""
if self.proc is not None:
poll_obj = select.poll()
poll_obj.register(self.proc.stdout, select.POLLIN)
Expand All @@ -45,13 +62,15 @@ def read_line(self, time_limit=None):
return None

def terminate(self, wait=False):
"""Terminate the process."""
if self.proc is not None:
self.proc.stdout.close()
self.proc.terminate()
if wait:
self.proc.wait()

def pid(self):
"""Get the process id. Returns none for non-running processes."""
if self.proc is not None:
return self.proc.pid
else:
Expand All @@ -60,6 +79,7 @@ def pid(self):
@staticmethod
def terminate_all(wait=False):
"""
Terminate all processes.
:param wait: Wait for each to terminate
:type wait: bool
Expand All @@ -72,19 +92,36 @@ def terminate_all(wait=False):

@staticmethod
def create_process(command):
"""
Create a process using this factory method. This does not start it.
:param command: A command to start. Parameters separated with spaces
or as a list, e.g. "command arg1 arg2" or ["command", "arg1", "arg2"].
"""
if getattr(select, "poll", None) is not None:
return Process(command)
else:
return WindowsProcess(command)


class WindowsProcess(Process):
"""A class for a Windows process."""

def __init__(self, command):
"""
Define a new process but do not start it.
:param command: A command to start. Parameters separated with spaces
or as a list, e.g. "command arg1 arg2" or ["command", "arg1", "arg2"].
"""
Process.__init__(self, command)

def read_line(self, time_limit=None):
""" Time limit has no effect.
The operation will always block on Windows."""
"""
Read a line from the process.
On Windows, this the time_limit has no effect, it always blocks.
"""
if self.proc is not None:
return self.proc.stdout.readline().decode()
else:
Expand All @@ -96,10 +133,15 @@ def read_line(self, time_limit=None):


class ProcessError(Exception):
"""A process-related exception."""

pass


class UnexpectedOutputError(ProcessError):
"""An unexpected output produced by the process."""

def __init__(self, output, expected=None):
"""Create an unexpected output exception."""
self.expected = expected
self.output = output
14 changes: 14 additions & 0 deletions sacredboard/app/process/tensorboard.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""Module for managing TensorBoard processes."""
import re

from sacredboard.app.process.process \
Expand All @@ -7,6 +8,7 @@


def stop_all_tensorboards():
"""Terminate all TensorBoard instances."""
for process in Process.instances:
print("Process '%s', running %d" % (process.command[0],
process.is_running()))
Expand All @@ -15,10 +17,22 @@ def stop_all_tensorboards():


class TensorboardNotFoundError(ProcessError):
"""TensorBoard binary not found."""

pass


def run_tensorboard(logdir, listen_on="0.0.0.0", tensorboard_args=None):
"""
Launch a new TensorBoard instance.
:param logdir: Path to a TensorFlow summary directory
:param listen_on: The IP address TensorBoard should listen on.
:param tensorboard_args: Additional TensorBoard arguments.
:return: Returns the port TensorBoard is listening on.
:raise UnexpectedOutputError
:raise TensorboardNotFoundError
"""
if tensorboard_args is None:
tensorboard_args = []
tensorboard_instance = Process.create_process(
Expand Down
1 change: 1 addition & 0 deletions sacredboard/app/webapi/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Defines ways of accessing Runs via HTTP interface."""
12 changes: 12 additions & 0 deletions sacredboard/app/webapi/routes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# coding=utf-8
"""Define HTTP endpoints for the Sacredboard Web API."""
import re
from pathlib import Path

Expand All @@ -17,27 +18,32 @@

@routes.route("/")
def index():
"""Redirect user to the main page."""
return redirect(url_for("routes.show_runs"))


@routes.route("/_tests")
def tests():
"""Redirect user to a page with JavaScript tests."""
return redirect(url_for("static", filename="scripts/tests/index.html"))


@routes.route("/runs")
def show_runs():
"""Render the main page with a list of experiment runs."""
# return render_template("runs.html", runs=data.runs(), type=type)
return render_template("runs.html", runs=[], type=type)


@routes.route("/api/run")
def api_runs():
"""Return a list of runs as a JSON object."""
return get_runs()


@routes.route("/api/run/<run_id>")
def api_run(run_id):
"""Return a single run as a JSON object."""
data = current_app.config["data"]
run = data.get_run(run_id)
records_total = 1 if run is not None else 0
Expand All @@ -59,6 +65,7 @@ def api_run(run_id):

@routes.route("/tensorboard/start/<run_id>/<int:tflog_id>")
def run_tensorboard(run_id, tflog_id):
"""Launch TensorBoard for a given run ID and log ID of that run."""
data = current_app.config["data"]
# optimisticaly suppose the run exists...
run = data.get_run(run_id)
Expand All @@ -79,27 +86,32 @@ def run_tensorboard(run_id, tflog_id):

@routes.route("/tensorboard/stop", methods=['GET', 'POST'])
def close_tensorboards():
"""Stop all TensorBoard instances launched by Sacredboard."""
stop_all_tensorboards()
return "Stopping tensorboard"


@routes.errorhandler(TensorboardNotFoundError)
def handle_tensorboard_not_found(e):
"""Handle exception: tensorboard script not found."""
return "Tensorboard not found on your system." \
" Please install tensorflow first. Sorry.", 503


@routes.errorhandler(TimeoutError)
def handle_tensorboard_timeout(e):
"""Handle exception: TensorBoard does not respond."""
return "Tensorboard does not respond. Sorry.", 503


@routes.errorhandler(process.UnexpectedOutputError)
def handle_tensorboard_unexpected_output(e: process.UnexpectedOutputError):
"""Handle Exception: TensorBoard has produced an unexpected output."""
return "Tensorboard outputted '%s'," \
" but the information expected was: '%s'. Sorry." \
% (e.output, e.expected), 503


def setup_routes(app):
"""Register all HTTP endpoints defined in this file."""
app.register_blueprint(routes)
4 changes: 4 additions & 0 deletions sacredboard/app/webapi/runs.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
# coding=utf-8
"""WebAPI module for handling run-related requests."""
import json

from flask import current_app, request, Response, render_template


def parse_int_arg(name, default):
"""Return a given URL parameter as int or return the default value."""
return default if request.args.get(name) is None \
else int(request.args.get(name))


def parse_query_filter():
"""Parse the Run query filter from the URL as a dictionary."""
query_string = request.args.get("queryFilter")
if query_string is None:
return {"type": "and", "filters": []}
Expand All @@ -20,6 +23,7 @@ def parse_query_filter():


def get_runs():
"""Get all runs, sort it and return a response."""
data = current_app.config["data"]
draw = parse_int_arg("draw", 1)
start = parse_int_arg("start", 0)
Expand Down
Loading

0 comments on commit dc4e87e

Please sign in to comment.