diff --git a/sacredboard/app/data/datastorage.py b/sacredboard/app/data/datastorage.py index bb3f1d5..4a0aae5 100644 --- a/sacredboard/app/data/datastorage.py +++ b/sacredboard/app/data/datastorage.py @@ -1,9 +1,11 @@ -"""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): @@ -11,16 +13,20 @@ def count(self): 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): diff --git a/sacredboard/app/data/filestorage.py b/sacredboard/app/data/filestorage.py index 8779da3..2c1daae 100644 --- a/sacredboard/app/data/filestorage.py +++ b/sacredboard/app/data/filestorage.py @@ -49,18 +49,20 @@ 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) @@ -68,14 +70,15 @@ 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 """ @@ -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 @@ -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) diff --git a/sacredboard/app/data/mongodb.py b/sacredboard/app/data/mongodb.py index 6242e9b..d12a1ee 100755 --- a/sacredboard/app/data/mongodb.py +++ b/sacredboard/app/data/mongodb.py @@ -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 diff --git a/sacredboard/app/process/__init__.py b/sacredboard/app/process/__init__.py index e69de29..5c22027 100644 --- a/sacredboard/app/process/__init__.py +++ b/sacredboard/app/process/__init__.py @@ -0,0 +1 @@ +"""Sacredboard module for launching external processes.""" diff --git a/sacredboard/app/process/process.py b/sacredboard/app/process/process.py index 0ea9ebd..c34a867 100644 --- a/sacredboard/app/process/process.py +++ b/sacredboard/app/process/process.py @@ -1,4 +1,5 @@ # coding=utf-8 +"""Module for launching processes and reading their output.""" import atexit import os import select @@ -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: @@ -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 @@ -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) @@ -45,6 +62,7 @@ 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() @@ -52,6 +70,7 @@ def terminate(self, wait=False): 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: @@ -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 @@ -72,6 +92,12 @@ 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: @@ -79,12 +105,23 @@ def create_process(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: @@ -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 diff --git a/sacredboard/app/process/tensorboard.py b/sacredboard/app/process/tensorboard.py index 477e535..570530c 100644 --- a/sacredboard/app/process/tensorboard.py +++ b/sacredboard/app/process/tensorboard.py @@ -1,3 +1,4 @@ +"""Module for managing TensorBoard processes.""" import re from sacredboard.app.process.process \ @@ -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())) @@ -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( diff --git a/sacredboard/app/webapi/__init__.py b/sacredboard/app/webapi/__init__.py index e69de29..dce0b49 100644 --- a/sacredboard/app/webapi/__init__.py +++ b/sacredboard/app/webapi/__init__.py @@ -0,0 +1 @@ +"""Defines ways of accessing Runs via HTTP interface.""" diff --git a/sacredboard/app/webapi/routes.py b/sacredboard/app/webapi/routes.py index f204408..e3c01a6 100644 --- a/sacredboard/app/webapi/routes.py +++ b/sacredboard/app/webapi/routes.py @@ -1,4 +1,5 @@ # coding=utf-8 +"""Define HTTP endpoints for the Sacredboard Web API.""" import re from pathlib import Path @@ -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/") 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 @@ -59,6 +65,7 @@ def api_run(run_id): @routes.route("/tensorboard/start//") 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) @@ -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) diff --git a/sacredboard/app/webapi/runs.py b/sacredboard/app/webapi/runs.py index daa7686..10e9669 100644 --- a/sacredboard/app/webapi/runs.py +++ b/sacredboard/app/webapi/runs.py @@ -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": []} @@ -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) diff --git a/sacredboard/bootstrap.py b/sacredboard/bootstrap.py index b68548d..4328d3c 100755 --- a/sacredboard/bootstrap.py +++ b/sacredboard/bootstrap.py @@ -37,7 +37,8 @@ "or Sacred v0.6 (which used default.runs). " "Default: runs") @click.option("-F", default="", - help="Path to directory containing experiments.") + help="Path to directory containing experiment results of the" + "File Storage observer.") @click.option("--no-browser", is_flag=True, default=False, help="Do not open web browser automatically.") @click.option("--debug", is_flag=True, default=False,