Skip to content
This repository was archived by the owner on Aug 11, 2020. It is now read-only.

Commit b96614e

Browse files
committed
Feature PS-9868:
Add Job logs to job group in cli
1 parent deb58e8 commit b96614e

File tree

8 files changed

+158
-182
lines changed

8 files changed

+158
-182
lines changed

paperspace/cli/cli.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from paperspace.cli import common
88
from paperspace.cli.common import api_key_option, del_if_value_is_none
99
from paperspace.cli.jobs import jobs_group
10-
from paperspace.cli.logs import logs_group
1110
from paperspace.cli.models import models_group
1211
from paperspace.cli.projects import projects_group
1312
from paperspace.cli.cli_types import ChoiceType, json_string
@@ -1068,7 +1067,6 @@ def version():
10681067
cli.add_command(jobs_group)
10691068
cli.add_command(projects_group)
10701069
cli.add_command(models_group)
1071-
cli.add_command(logs_group)
10721070

10731071

10741072
if __name__ == '__main__':

paperspace/cli/jobs.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,16 @@ def list_jobs(api_key, **filters):
6060
jobs_api = client.API(config.CONFIG_HOST, api_key=api_key)
6161
command = jobs_commands.ListJobsCommand(api=jobs_api)
6262
command.execute(filters)
63+
64+
65+
@jobs_group.command("log", help="List job logs")
66+
@click.option(
67+
"--jobId",
68+
"job_id",
69+
required=True
70+
)
71+
@common.api_key_option
72+
def list_logs(job_id, api_key=None):
73+
logs_api = client.API(config.CONFIG_LOG_HOST, api_key=api_key)
74+
command = jobs_commands.JobLogsCommand(api=logs_api)
75+
command.execute(job_id)

paperspace/cli/logs.py

Lines changed: 0 additions & 23 deletions
This file was deleted.

paperspace/commands/jobs.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pydoc
22

33
import terminaltables
4+
from click import style
45

56
from paperspace.commands import CommandBase
67
from paperspace.utils import get_terminal_lines
@@ -82,3 +83,56 @@ def _make_table(jobs):
8283
ascii_table = terminaltables.AsciiTable(data)
8384
table_string = ascii_table.table
8485
return table_string
86+
87+
88+
class JobLogsCommand(CommandBase):
89+
last_line_number = 0
90+
base_url = "/jobs/logs?jobId={}&line={}"
91+
92+
is_logs_complete = False
93+
94+
def execute(self, job_id):
95+
table_title = "Job %s logs" % job_id
96+
table_data = [("LINE", "MESSAGE")]
97+
table = terminaltables.AsciiTable(table_data, title=table_title)
98+
99+
while not self.is_logs_complete:
100+
response = self._get_logs(job_id)
101+
102+
try:
103+
data = response.json()
104+
if not response.ok:
105+
self.logger.log_error_response(data)
106+
return
107+
except (ValueError, KeyError) as e:
108+
if response.status_code == 204:
109+
continue
110+
self.logger.log("Error while parsing response data: {}".format(e))
111+
return
112+
else:
113+
self._log_logs_list(data, table, table_data)
114+
115+
def _get_logs(self, job_id):
116+
url = self.base_url.format(job_id, self.last_line_number)
117+
return self.api.get(url)
118+
119+
def _log_logs_list(self, data, table, table_data):
120+
if not data:
121+
self.logger.log("No Logs found")
122+
else:
123+
table_str = self._make_table(data, table, table_data)
124+
if len(table_str.splitlines()) > get_terminal_lines():
125+
pydoc.pager(table_str)
126+
else:
127+
self.logger.log(table_str)
128+
129+
def _make_table(self, logs, table, table_data):
130+
if logs[-1].get("message") == "PSEOF":
131+
self.is_logs_complete = True
132+
else:
133+
self.last_line_number = logs[-1].get("line")
134+
135+
for log in logs:
136+
table_data.append((style(fg="red", text=str(log.get("line"))), log.get("message")))
137+
138+
return table.table

paperspace/commands/logs.py

Lines changed: 0 additions & 60 deletions
This file was deleted.

tests/functional/test_jobs.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,93 @@ def test_should_print_proper_message_when_jobs_list_was_used_with_mutually_exclu
167167
params=None)
168168
assert result.output == self.EXPECTED_STDOUT_WHEN_MUTUALLY_EXCLUSIVE_FILTERS
169169
assert result.exit_code == 0
170+
171+
172+
class TestJobLogs(object):
173+
URL = "https://logs.paperspace.io/jobs/logs?jobId=some_job_id&line=0"
174+
EXPECTED_HEADERS = default_headers.copy()
175+
EXPECTED_HEADERS_WITH_CHANGED_API_KEY = default_headers.copy()
176+
EXPECTED_HEADERS_WITH_CHANGED_API_KEY["X-API-Key"] = "some_key"
177+
178+
RESPONSE_JSON_WITH_WRONG_API_TOKEN = {"status": 400, "message": "Invalid API token"}
179+
EXPECTED_RESPONSE_JSON = example_responses.LIST_OF_LOGS_FOR_JOB
180+
BASIC_COMMAND_WITHOUT_PARAMETERS = ["jobs", "log"]
181+
BASIC_COMMAND = ["jobs", "log", "--jobId", "some_job_id", "--apiKey", "some_key"]
182+
183+
EXPECTED_STDOUT_WITHOUT_PARAMETERS = """Usage: cli jobs log [OPTIONS]
184+
Try "cli jobs log --help" for help.
185+
186+
Error: Missing option "--jobId".
187+
"""
188+
189+
EXPECTED_STDOUT = """+Job some_job_id logs--------------------------------------------------------------------+
190+
| LINE | MESSAGE |
191+
+------+---------------------------------------------------------------------------------+
192+
| 1 | Traceback (most recent call last): |
193+
| 2 | File "generate_figures.py", line 15, in <module> |
194+
| 3 | import dnnlib.tflib as tflib |
195+
| 4 | File "/paperspace/dnnlib/tflib/__init__.py", line 8, in <module> |
196+
| 5 | from . import autosummary |
197+
| 6 | File "/paperspace/dnnlib/tflib/autosummary.py", line 31, in <module> |
198+
| 7 | from . import tfutil |
199+
| 8 | File "/paperspace/dnnlib/tflib/tfutil.py", line 34, in <module> |
200+
| 9 | def shape_to_list(shape: Iterable[tf.Dimension]) -> List[Union[int, None]]: |
201+
| 10 | AttributeError: module 'tensorflow' has no attribute 'Dimension' |
202+
| 11 | PSEOF |
203+
+------+---------------------------------------------------------------------------------+
204+
"""
205+
206+
EXPECTED_STDOUT_WITH_WRONG_API_TOKEN = "Invalid API token\n"
207+
208+
@mock.patch("paperspace.cli.cli.client.requests.get")
209+
def test_command_should_not_send_request_without_required_parameters(self, get_patched):
210+
cli_runner = CliRunner()
211+
result = cli_runner.invoke(cli.cli, self.BASIC_COMMAND_WITHOUT_PARAMETERS)
212+
print(result)
213+
214+
get_patched.assert_not_called()
215+
assert result.exit_code == 2
216+
assert result.output == self.EXPECTED_STDOUT_WITHOUT_PARAMETERS
217+
218+
@mock.patch("paperspace.cli.cli.client.requests.get")
219+
def test_should_send_valid_get_request_and_print_available_logs(self, get_patched):
220+
get_patched.return_value = MockResponse(json_data=self.EXPECTED_RESPONSE_JSON, status_code=200)
221+
222+
cli_runner = CliRunner()
223+
result = cli_runner.invoke(cli.cli, self.BASIC_COMMAND)
224+
225+
get_patched.assert_called_with(self.URL,
226+
headers=self.EXPECTED_HEADERS_WITH_CHANGED_API_KEY,
227+
json=None,
228+
params=None)
229+
230+
assert result.output == self.EXPECTED_STDOUT
231+
assert result.exit_code == 0
232+
233+
@mock.patch("paperspace.cli.cli.client.requests.get")
234+
def test_should_send_valid_get_request_when_log_list_was_used_with_wrong_api_key(self, get_patched):
235+
get_patched.return_value = MockResponse(json_data=self.RESPONSE_JSON_WITH_WRONG_API_TOKEN, status_code=400)
236+
237+
cli_runner = CliRunner()
238+
result = cli_runner.invoke(cli.cli, self.BASIC_COMMAND)
239+
240+
get_patched.assert_called_with(self.URL,
241+
headers=self.EXPECTED_HEADERS_WITH_CHANGED_API_KEY,
242+
json=None,
243+
params=None)
244+
assert result.output == self.EXPECTED_STDOUT_WITH_WRONG_API_TOKEN
245+
assert result.exit_code == 0
246+
247+
@mock.patch("paperspace.cli.cli.client.requests.get")
248+
def test_should_print_error_message_when_error_status_code_received_but_no_content_was_provided(self, get_patched):
249+
get_patched.return_value = MockResponse(status_code=400)
250+
251+
cli_runner = CliRunner()
252+
result = cli_runner.invoke(cli.cli, self.BASIC_COMMAND)
253+
254+
get_patched.assert_called_with(self.URL,
255+
headers=self.EXPECTED_HEADERS_WITH_CHANGED_API_KEY,
256+
json=None,
257+
params=None)
258+
assert result.output == "Error while parsing response data: No JSON\n"
259+
assert result.exit_code == 0

tests/functional/test_logs.py

Lines changed: 0 additions & 96 deletions
This file was deleted.

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ deps =
2626
pip_pre = true
2727

2828
commands =
29-
pytest --cov=paperspace --cov-append
29+
pytest -vv --cov=paperspace --cov-append
3030

3131
[testenv:check]
3232
deps =

0 commit comments

Comments
 (0)