Skip to content

Commit

Permalink
uploader: add --json flag to the list command (tensorflow#3480)
Browse files Browse the repository at this point in the history
* Motivation for features / changes
  * Fulfill feature request bug b/153232102
* Technical description of changes
  * Add the `--json` flag to the `list` subcommand of `tensorboard dev`.
  * If the flag is used, the experiments will be printed as a JSON object mapping experiment URLs to experiment data (name, description, runs, tags, etc.)
* Screenshots of UI changes
  * ![image](https://user-images.githubusercontent.com/16824702/78626883-0f77f480-785e-11ea-88ca-b8d653d302c6.png)
* Detailed steps to verify changes work correctly (as executed by you)
  * Manually ran `tensorboard dev list --json` (see screenshot above)
* Alternate designs / implementations considered
  * Output a single big json array at the end:
    * Pro: may be easier to parse programmatically
    * Con: no streaming
  • Loading branch information
caisq authored and bileschi committed Apr 15, 2020
1 parent aede232 commit ec16529
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 14 deletions.
21 changes: 21 additions & 0 deletions tensorboard/uploader/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,26 @@ py_test(
],
)

py_library(
name = "formatters",
srcs = ["formatters.py"],
srcs_version = "PY3",
deps = [
":util",
],
)

py_test(
name = "formatters_test",
srcs = ["formatters_test.py"],
deps = [
":formatters",
":util",
"//tensorboard:test",
"//tensorboard/uploader/proto:protos_all_py_pb2",
],
)

py_binary(
name = "uploader",
srcs = ["uploader_main.py"],
Expand All @@ -60,6 +80,7 @@ py_library(
":dev_creds",
":exporter_lib",
":flags_parser",
":formatters",
":server_info",
":uploader_lib",
":util",
Expand Down
5 changes: 5 additions & 0 deletions tensorboard/uploader/flags_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,11 @@ def define_flags(parser):
"list", help="list previously uploaded experiments"
)
list_parser.set_defaults(**{SUBCOMMAND_FLAG: SUBCOMMAND_KEY_LIST})
list_parser.add_argument(
"--json",
action="store_true",
help="print the experiments as JSON objects",
)

export = subparsers.add_parser(
"export", help="download all your experiment data"
Expand Down
99 changes: 99 additions & 0 deletions tensorboard/uploader/formatters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Copyright 2020 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Helpers that format the information about experiments as strings."""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import abc
import collections
import json

from tensorboard.uploader import util


class BaseExperimentFormatter(object):
"""Abstract base class for formatting experiment information as a string."""

__metaclass__ = abc.ABCMeta

@abc.abstractmethod
def format_experiment(self, experiment, experiment_url):
"""Format the information about an experiment as a representing string.
Args:
experiment: An `experiment_pb2.Experiment` protobuf message for the
experiment to be formatted.
experiment_url: The URL at which the experiment can be accessed via
TensorBoard.
Returns:
A string that represents the experiment.
"""
pass


class ReadableFormatter(BaseExperimentFormatter):
"""A formatter implementation that outputs human-readable text."""

_NAME_COLUMN_WIDTH = 12

def __init__(self):
super(ReadableFormatter, self).__init__()

def format_experiment(self, experiment, experiment_url):
output = []
output.append(experiment_url)
data = [
("Name", experiment.name or "[No Name]"),
("Description", experiment.description or "[No Description]"),
("Id", experiment.experiment_id),
("Created", util.format_time(experiment.create_time)),
("Updated", util.format_time(experiment.update_time)),
("Runs", str(experiment.num_runs)),
("Tags", str(experiment.num_tags)),
("Scalars", str(experiment.num_scalars)),
]
for name, value in data:
output.append(
"\t%s %s" % (name.ljust(self._NAME_COLUMN_WIDTH), value,)
)
return "\n".join(output)


class JsonFormatter(object):
"""A formatter implementation: outputs experiment as JSON."""

_JSON_INDENT = 2

def __init__(self):
super(JsonFormatter, self).__init__()

def format_experiment(self, experiment, experiment_url):
data = [
("url", experiment_url),
("name", experiment.name),
("description", experiment.description),
("id", experiment.experiment_id),
("created", util.format_time_absolute(experiment.create_time)),
("updated", util.format_time_absolute(experiment.update_time)),
("runs", experiment.num_runs),
("tags", experiment.num_tags),
("scalars", experiment.num_scalars),
]
return json.dumps(
collections.OrderedDict(data), indent=self._JSON_INDENT,
)
113 changes: 113 additions & 0 deletions tensorboard/uploader/formatters_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Copyright 2020 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
# Lint as: python3
"""Tests for tensorboard.uploader.formatters."""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from tensorboard import test as tb_test
from tensorboard.uploader import formatters
from tensorboard.uploader.proto import experiment_pb2

from tensorboard.uploader import util


class TensorBoardExporterTest(tb_test.TestCase):
def testReadableFormatterWithNonemptyNameAndDescription(self):
experiment = experiment_pb2.Experiment(
experiment_id="deadbeef",
name="A name for the experiment",
description="A description for the experiment",
num_runs=2,
num_tags=4,
num_scalars=60,
)
util.set_timestamp(experiment.create_time, 981173106)
util.set_timestamp(experiment.update_time, 1015218367)
experiment_url = "http://tensorboard.dev/deadbeef"
formatter = formatters.ReadableFormatter()
output = formatter.format_experiment(experiment, experiment_url)
expected_lines = [
"http://tensorboard.dev/deadbeef",
"\tName A name for the experiment",
"\tDescription A description for the experiment",
"\tId deadbeef",
"\tCreated 2001-02-03 04:05:06",
"\tUpdated 2002-03-04 05:06:07",
"\tRuns 2",
"\tTags 4",
"\tScalars 60",
]
self.assertEqual(output.split("\n"), expected_lines)

def testReadableFormatterWithEmptyNameAndDescription(self):
experiment = experiment_pb2.Experiment(
experiment_id="deadbeef",
# NOTE(cais): `name` and `description` are missing here.
num_runs=2,
num_tags=4,
num_scalars=60,
)
util.set_timestamp(experiment.create_time, 981173106)
util.set_timestamp(experiment.update_time, 1015218367)
experiment_url = "http://tensorboard.dev/deadbeef"
formatter = formatters.ReadableFormatter()
output = formatter.format_experiment(experiment, experiment_url)
expected_lines = [
"http://tensorboard.dev/deadbeef",
"\tName [No Name]",
"\tDescription [No Description]",
"\tId deadbeef",
"\tCreated 2001-02-03 04:05:06",
"\tUpdated 2002-03-04 05:06:07",
"\tRuns 2",
"\tTags 4",
"\tScalars 60",
]
self.assertEqual(output.split("\n"), expected_lines)

def testJsonFormatterWithEmptyNameAndDescription(self):
experiment = experiment_pb2.Experiment(
experiment_id="deadbeef",
# NOTE(cais): `name` and `description` are missing here.
num_runs=2,
num_tags=4,
num_scalars=60,
)
util.set_timestamp(experiment.create_time, 981173106)
util.set_timestamp(experiment.update_time, 1015218367)
experiment_url = "http://tensorboard.dev/deadbeef"
formatter = formatters.JsonFormatter()
output = formatter.format_experiment(experiment, experiment_url)
expected_lines = [
"{",
' "url": "http://tensorboard.dev/deadbeef",',
' "name": "",',
' "description": "",',
' "id": "deadbeef",',
' "created": "2001-02-03T04:05:06Z",',
' "updated": "2002-03-04T05:06:07Z",',
' "runs": 2,',
' "tags": 4,',
' "scalars": 60',
"}",
]
self.assertEqual(output.split("\n"), expected_lines)


if __name__ == "__main__":
tb_test.main()
31 changes: 17 additions & 14 deletions tensorboard/uploader/uploader_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from tensorboard.uploader import auth
from tensorboard.uploader import exporter as exporter_lib
from tensorboard.uploader import flags_parser
from tensorboard.uploader import formatters
from tensorboard.uploader import server_info as server_info_lib
from tensorboard.uploader import uploader as uploader_lib
from tensorboard.uploader import util
Expand Down Expand Up @@ -332,6 +333,15 @@ class _ListIntent(_Intent):
"""
)

def __init__(self, json=None):
"""Constructor of _ListIntent.
Args:
json: If and only if `True`, will print the list as pretty-formatted
JSON objects, one object for each experiment.
"""
self.json = json

def get_ack_message_body(self):
return self._MESSAGE

Expand All @@ -348,23 +358,16 @@ def execute(self, server_info, channel):
)
gen = exporter_lib.list_experiments(api_client, fieldmask=fieldmask)
count = 0

if self.json:
formatter = formatters.JsonFormatter()
else:
formatter = formatters.ReadableFormatter()
for experiment in gen:
count += 1
experiment_id = experiment.experiment_id
url = server_info_lib.experiment_url(server_info, experiment_id)
print(url)
data = [
("Name", experiment.name or "[No Name]"),
("Description", experiment.description or "[No Description]"),
("Id", experiment.experiment_id),
("Created", util.format_time(experiment.create_time)),
("Updated", util.format_time(experiment.update_time)),
("Scalars", str(experiment.num_scalars)),
("Runs", str(experiment.num_runs)),
("Tags", str(experiment.num_tags)),
]
for (name, value) in data:
print("\t%s %s" % (name.ljust(12), value))
print(formatter.format_experiment(experiment, url))
sys.stdout.flush()
if not count:
sys.stderr.write(
Expand Down Expand Up @@ -550,7 +553,7 @@ def _get_intent(flags):
"Must specify experiment to delete via `--experiment_id`."
)
elif cmd == flags_parser.SUBCOMMAND_KEY_LIST:
return _ListIntent()
return _ListIntent(json=flags.json)
elif cmd == flags_parser.SUBCOMMAND_KEY_EXPORT:
if flags.outdir:
return _ExportIntent(flags.outdir)
Expand Down

0 comments on commit ec16529

Please sign in to comment.