Skip to content
This repository was archived by the owner on Aug 11, 2020. It is now read-only.
Prev Previous commit
Next Next commit
Feature PS-9868:
Add Job logs to job group in cli
  • Loading branch information
MudlaffP committed May 6, 2019
commit b96614ec1272336778c264c68f4adaa53b278363
2 changes: 0 additions & 2 deletions paperspace/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from paperspace.cli import common
from paperspace.cli.common import api_key_option, del_if_value_is_none
from paperspace.cli.jobs import jobs_group
from paperspace.cli.logs import logs_group
from paperspace.cli.models import models_group
from paperspace.cli.projects import projects_group
from paperspace.cli.cli_types import ChoiceType, json_string
Expand Down Expand Up @@ -1068,7 +1067,6 @@ def version():
cli.add_command(jobs_group)
cli.add_command(projects_group)
cli.add_command(models_group)
cli.add_command(logs_group)


if __name__ == '__main__':
Expand Down
13 changes: 13 additions & 0 deletions paperspace/cli/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,16 @@ def list_jobs(api_key, **filters):
jobs_api = client.API(config.CONFIG_HOST, api_key=api_key)
command = jobs_commands.ListJobsCommand(api=jobs_api)
command.execute(filters)


@jobs_group.command("log", help="List job logs")
@click.option(
"--jobId",
"job_id",
required=True
)
@common.api_key_option
def list_logs(job_id, api_key=None):
logs_api = client.API(config.CONFIG_LOG_HOST, api_key=api_key)
command = jobs_commands.JobLogsCommand(api=logs_api)
command.execute(job_id)
23 changes: 0 additions & 23 deletions paperspace/cli/logs.py

This file was deleted.

54 changes: 54 additions & 0 deletions paperspace/commands/jobs.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pydoc

import terminaltables
from click import style

from paperspace.commands import CommandBase
from paperspace.utils import get_terminal_lines
Expand Down Expand Up @@ -82,3 +83,56 @@ def _make_table(jobs):
ascii_table = terminaltables.AsciiTable(data)
table_string = ascii_table.table
return table_string


class JobLogsCommand(CommandBase):
last_line_number = 0
base_url = "/jobs/logs?jobId={}&line={}"

is_logs_complete = False

def execute(self, job_id):
table_title = "Job %s logs" % job_id
table_data = [("LINE", "MESSAGE")]
table = terminaltables.AsciiTable(table_data, title=table_title)

while not self.is_logs_complete:
response = self._get_logs(job_id)

try:
data = response.json()
if not response.ok:
self.logger.log_error_response(data)
return
except (ValueError, KeyError) as e:
if response.status_code == 204:
continue
self.logger.log("Error while parsing response data: {}".format(e))
return
else:
self._log_logs_list(data, table, table_data)

def _get_logs(self, job_id):
url = self.base_url.format(job_id, self.last_line_number)
return self.api.get(url)

def _log_logs_list(self, data, table, table_data):
if not data:
self.logger.log("No Logs found")
else:
table_str = self._make_table(data, table, table_data)
if len(table_str.splitlines()) > get_terminal_lines():
pydoc.pager(table_str)
else:
self.logger.log(table_str)

def _make_table(self, logs, table, table_data):
if logs[-1].get("message") == "PSEOF":
self.is_logs_complete = True
else:
self.last_line_number = logs[-1].get("line")

for log in logs:
table_data.append((style(fg="red", text=str(log.get("line"))), log.get("message")))

return table.table
60 changes: 0 additions & 60 deletions paperspace/commands/logs.py

This file was deleted.

90 changes: 90 additions & 0 deletions tests/functional/test_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,93 @@ def test_should_print_proper_message_when_jobs_list_was_used_with_mutually_exclu
params=None)
assert result.output == self.EXPECTED_STDOUT_WHEN_MUTUALLY_EXCLUSIVE_FILTERS
assert result.exit_code == 0


class TestJobLogs(object):
URL = "https://logs.paperspace.io/jobs/logs?jobId=some_job_id&line=0"
EXPECTED_HEADERS = default_headers.copy()
EXPECTED_HEADERS_WITH_CHANGED_API_KEY = default_headers.copy()
EXPECTED_HEADERS_WITH_CHANGED_API_KEY["X-API-Key"] = "some_key"

RESPONSE_JSON_WITH_WRONG_API_TOKEN = {"status": 400, "message": "Invalid API token"}
EXPECTED_RESPONSE_JSON = example_responses.LIST_OF_LOGS_FOR_JOB
BASIC_COMMAND_WITHOUT_PARAMETERS = ["jobs", "log"]
BASIC_COMMAND = ["jobs", "log", "--jobId", "some_job_id", "--apiKey", "some_key"]

EXPECTED_STDOUT_WITHOUT_PARAMETERS = """Usage: cli jobs log [OPTIONS]
Try "cli jobs log --help" for help.

Error: Missing option "--jobId".
"""

EXPECTED_STDOUT = """+Job some_job_id logs--------------------------------------------------------------------+
| LINE | MESSAGE |
+------+---------------------------------------------------------------------------------+
| 1 | Traceback (most recent call last): |
| 2 | File "generate_figures.py", line 15, in <module> |
| 3 | import dnnlib.tflib as tflib |
| 4 | File "/paperspace/dnnlib/tflib/__init__.py", line 8, in <module> |
| 5 | from . import autosummary |
| 6 | File "/paperspace/dnnlib/tflib/autosummary.py", line 31, in <module> |
| 7 | from . import tfutil |
| 8 | File "/paperspace/dnnlib/tflib/tfutil.py", line 34, in <module> |
| 9 | def shape_to_list(shape: Iterable[tf.Dimension]) -> List[Union[int, None]]: |
| 10 | AttributeError: module 'tensorflow' has no attribute 'Dimension' |
| 11 | PSEOF |
+------+---------------------------------------------------------------------------------+
"""

EXPECTED_STDOUT_WITH_WRONG_API_TOKEN = "Invalid API token\n"

@mock.patch("paperspace.cli.cli.client.requests.get")
def test_command_should_not_send_request_without_required_parameters(self, get_patched):
cli_runner = CliRunner()
result = cli_runner.invoke(cli.cli, self.BASIC_COMMAND_WITHOUT_PARAMETERS)
print(result)

get_patched.assert_not_called()
assert result.exit_code == 2
assert result.output == self.EXPECTED_STDOUT_WITHOUT_PARAMETERS

@mock.patch("paperspace.cli.cli.client.requests.get")
def test_should_send_valid_get_request_and_print_available_logs(self, get_patched):
get_patched.return_value = MockResponse(json_data=self.EXPECTED_RESPONSE_JSON, status_code=200)

cli_runner = CliRunner()
result = cli_runner.invoke(cli.cli, self.BASIC_COMMAND)

get_patched.assert_called_with(self.URL,
headers=self.EXPECTED_HEADERS_WITH_CHANGED_API_KEY,
json=None,
params=None)

assert result.output == self.EXPECTED_STDOUT
assert result.exit_code == 0

@mock.patch("paperspace.cli.cli.client.requests.get")
def test_should_send_valid_get_request_when_log_list_was_used_with_wrong_api_key(self, get_patched):
get_patched.return_value = MockResponse(json_data=self.RESPONSE_JSON_WITH_WRONG_API_TOKEN, status_code=400)

cli_runner = CliRunner()
result = cli_runner.invoke(cli.cli, self.BASIC_COMMAND)

get_patched.assert_called_with(self.URL,
headers=self.EXPECTED_HEADERS_WITH_CHANGED_API_KEY,
json=None,
params=None)
assert result.output == self.EXPECTED_STDOUT_WITH_WRONG_API_TOKEN
assert result.exit_code == 0

@mock.patch("paperspace.cli.cli.client.requests.get")
def test_should_print_error_message_when_error_status_code_received_but_no_content_was_provided(self, get_patched):
get_patched.return_value = MockResponse(status_code=400)

cli_runner = CliRunner()
result = cli_runner.invoke(cli.cli, self.BASIC_COMMAND)

get_patched.assert_called_with(self.URL,
headers=self.EXPECTED_HEADERS_WITH_CHANGED_API_KEY,
json=None,
params=None)
assert result.output == "Error while parsing response data: No JSON\n"
assert result.exit_code == 0
96 changes: 0 additions & 96 deletions tests/functional/test_logs.py

This file was deleted.

2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ deps =
pip_pre = true

commands =
pytest --cov=paperspace --cov-append
pytest -vv --cov=paperspace --cov-append

[testenv:check]
deps =
Expand Down