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

Commit eb4caa0

Browse files
committed
Add 'projects list' command
1 parent 19a6193 commit eb4caa0

File tree

6 files changed

+374
-1
lines changed

6 files changed

+374
-1
lines changed

paperspace/cli/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from paperspace import constants, client, config
77
from paperspace.cli.common import api_key_option, del_if_value_is_none
88
from paperspace.cli.jobs import jobs_group
9+
from paperspace.cli.projects import projects_group
910
from paperspace.cli.types import ChoiceType, json_string
1011
from paperspace.cli.validators import validate_mutually_exclusive, validate_email
1112
from paperspace.commands import experiments as experiments_commands, deployments as deployments_commands, \
@@ -1052,3 +1053,4 @@ def version():
10521053

10531054

10541055
cli.add_command(jobs_group)
1056+
cli.add_command(projects_group)

paperspace/cli/projects.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import click
2+
3+
from paperspace import client, config
4+
from paperspace.commands import projects as projects_commands
5+
from . import common
6+
7+
8+
@click.group("projects", help="Manage projects")
9+
def projects_group():
10+
pass
11+
12+
13+
@projects_group.command("list", help="List projects")
14+
@common.api_key_option
15+
def delete_job(api_key):
16+
projects_api = client.API(config.CONFIG_HOST, api_key=api_key)
17+
command = projects_commands.ListProjectsCommand(api=projects_api)
18+
command.execute()

paperspace/commands/projects.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import pydoc
2+
3+
import terminaltables
4+
5+
from paperspace import client, config, version, logger
6+
from paperspace.utils import get_terminal_lines
7+
8+
default_headers = {"X-API-Key": config.PAPERSPACE_API_KEY,
9+
"ps_client_name": "paperspace-python",
10+
"ps_client_version": version.version}
11+
deployments_api = client.API(config.CONFIG_HOST, headers=default_headers)
12+
13+
14+
class ProjectsCommandBase(object):
15+
def __init__(self, api=deployments_api, logger_=logger):
16+
self.api = api
17+
self.logger = logger_
18+
19+
20+
class ListProjectsCommand(ProjectsCommandBase):
21+
def execute(self):
22+
# TODO: PS_API should not require teamId but it does now, so change the following line
23+
# TODO: to `json_ = None` or whatever works when PS_API is fixed:
24+
json_ = {"teamId": 666}
25+
response = self.api.get("/projects/", json=json_)
26+
27+
try:
28+
data = response.json()
29+
if not response.ok:
30+
self.logger.log_error_response(data)
31+
return
32+
except (ValueError, KeyError) as e:
33+
self.logger.log("Error while parsing response data: {}".format(e))
34+
else:
35+
self._log_projects_list(data)
36+
37+
def _log_projects_list(self, data):
38+
if not data.get("data"):
39+
self.logger.log("No projects found")
40+
else:
41+
table_str = self._make_table(data["data"])
42+
if len(table_str.splitlines()) > get_terminal_lines():
43+
pydoc.pager(table_str)
44+
else:
45+
self.logger.log(table_str)
46+
47+
@staticmethod
48+
def _make_table(projects):
49+
data = [("ID", "Name", "Repository")]
50+
for project in projects:
51+
id_ = project.get("handle")
52+
name = project.get("name")
53+
repo_url = project.get("repoUrl")
54+
data.append((id_, name, repo_url))
55+
56+
ascii_table = terminaltables.AsciiTable(data)
57+
table_string = ascii_table.table
58+
return table_string

paperspace/main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88

99

1010
def main():
11-
if len(sys.argv) >= 2 and sys.argv[1] in ('experiments', 'deployments', 'machines', 'login', 'logout', 'version'):
11+
if len(sys.argv) >= 2 and sys.argv[1] in ('experiments', 'deployments', 'machines', 'login', 'logout', 'version',
12+
'projects', 'jobs'):
1213
cli(sys.argv[1:])
1314

1415
args = sys.argv[:]

tests/example_responses.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,3 +1273,192 @@
12731273
"billingMonth": "2019-04",
12741274
},
12751275
}
1276+
1277+
1278+
LIST_PROJECTS_RESPONSE = {
1279+
"data": [
1280+
{
1281+
"name": "test_project",
1282+
"handle": "prq70zy79",
1283+
"dtCreated": "2019-03-18T13:24:46.666Z",
1284+
"dtDeleted": None,
1285+
"lastJobSeqNum": 2,
1286+
"repoNodeId": None,
1287+
"repoName": None,
1288+
"repoUrl": None,
1289+
"experiments": {
1290+
"data": [
1291+
{
1292+
"dtCreated": "2019-04-05T15:10:55.692629+00:00",
1293+
"dtDeleted": None,
1294+
"dtFinished": None,
1295+
"dtModified": "2019-04-05T15:10:55.692629+00:00",
1296+
"dtProvisioningFinished": None,
1297+
"dtProvisioningStarted": None,
1298+
"dtStarted": None,
1299+
"dtTeardownFinished": None,
1300+
"dtTeardownStarted": None,
1301+
"experimentError": None,
1302+
"experimentTemplateHistoryId": 22159,
1303+
"experimentTemplateId": 60,
1304+
"experimentTypeId": 1,
1305+
"handle": "estgcoux8igx32",
1306+
"id": 22123,
1307+
"projectHandle": "prq70zy79",
1308+
"projectId": 612,
1309+
"started_by_user_id": 1655,
1310+
"state": 1,
1311+
"templateHistory": {
1312+
"dtCreated": "2019-04-05T15:10:54.923725+00:00",
1313+
"dtDeleted": None,
1314+
"experimentTemplateId": 60,
1315+
"id": 22159,
1316+
"params": {
1317+
"is_preemptible": False,
1318+
"name": "dsfads",
1319+
"ports": 5000,
1320+
"project_handle": "prq70zy79",
1321+
"worker_command": "sadas",
1322+
"worker_container": "asd",
1323+
"worker_machine_type": "C2",
1324+
"worker_use_dockerfile": False,
1325+
"workspaceUrl": "example.com"
1326+
},
1327+
"triggerEvent": None,
1328+
"triggerEventId": None
1329+
}
1330+
}
1331+
],
1332+
"meta": {
1333+
"itemGroup": {
1334+
"key": "projectHandle",
1335+
"value": "prq70zy79"
1336+
},
1337+
"totalItems": 1
1338+
}
1339+
}
1340+
},
1341+
{
1342+
"name": "keton",
1343+
"handle": "prmr22ve0",
1344+
"dtCreated": "2019-03-25T14:50:43.202Z",
1345+
"dtDeleted": None,
1346+
"lastJobSeqNum": 8,
1347+
"repoNodeId": None,
1348+
"repoName": None,
1349+
"repoUrl": None,
1350+
"experiments": {
1351+
"data": [
1352+
{
1353+
"dtCreated": "2019-04-02T15:17:03.393886+00:00",
1354+
"dtDeleted": None,
1355+
"dtFinished": "2019-04-02T17:02:54.654569+00:00",
1356+
"dtModified": "2019-04-02T15:17:03.393886+00:00",
1357+
"dtProvisioningFinished": "2019-04-02T15:17:10.978198+00:00",
1358+
"dtProvisioningStarted": "2019-04-02T15:17:10.978198+00:00",
1359+
"dtStarted": "2019-04-02T15:17:10.978198+00:00",
1360+
"dtTeardownFinished": "2019-04-02T17:02:54.654569+00:00",
1361+
"dtTeardownStarted": "2019-04-02T17:02:54.654569+00:00",
1362+
"experimentError": None,
1363+
"experimentTemplateHistoryId": 22130,
1364+
"experimentTemplateId": 174,
1365+
"experimentTypeId": 1,
1366+
"handle": "ehla1kvbwzaco",
1367+
"id": 22094,
1368+
"projectHandle": "prmr22ve0",
1369+
"projectId": 626,
1370+
"started_by_user_id": 1655,
1371+
"state": 5,
1372+
"templateHistory": {
1373+
"dtCreated": "2019-04-02T15:17:02.663449+00:00",
1374+
"dtDeleted": None,
1375+
"experimentTemplateId": 174,
1376+
"id": 22130,
1377+
"params": {
1378+
"is_preemptible": False,
1379+
"model_path": "/artifacts",
1380+
"model_type": "Tensorflow",
1381+
"name": "Test1",
1382+
"ports": 5000,
1383+
"project_handle": "prmr22ve0",
1384+
"worker_command": "python mnist.py --data_format=channels_last",
1385+
"worker_container": "tensorflow/tensorflow:1.13.1-py3",
1386+
"worker_machine_type": "K80",
1387+
"workspaceUrl": "https://github.com/Paperspace/mnist-sample"
1388+
},
1389+
"triggerEvent": None,
1390+
"triggerEventId": None
1391+
}
1392+
}
1393+
],
1394+
"meta": {
1395+
"itemGroup": {
1396+
"key": "projectHandle",
1397+
"value": "prmr22ve0"
1398+
},
1399+
"totalItems": 1
1400+
}
1401+
}
1402+
},
1403+
{
1404+
"name": "paperspace-python",
1405+
"handle": "przhbct98",
1406+
"dtCreated": "2019-04-04T15:12:34.229Z",
1407+
"dtDeleted": None,
1408+
"lastJobSeqNum": 3,
1409+
"repoNodeId": None,
1410+
"repoName": None,
1411+
"repoUrl": None,
1412+
"experiments": {
1413+
"data": [
1414+
{
1415+
"dtCreated": "2019-04-24T10:18:30.523193+00:00",
1416+
"dtDeleted": None,
1417+
"dtFinished": "2019-04-24T10:18:43.613748+00:00",
1418+
"dtModified": "2019-04-24T10:18:30.523193+00:00",
1419+
"dtProvisioningFinished": "2019-04-24T10:18:35.010792+00:00",
1420+
"dtProvisioningStarted": "2019-04-24T10:18:35.010792+00:00",
1421+
"dtStarted": "2019-04-24T10:18:35.010792+00:00",
1422+
"dtTeardownFinished": "2019-04-24T10:18:43.613748+00:00",
1423+
"dtTeardownStarted": "2019-04-24T10:18:43.613748+00:00",
1424+
"experimentError": None,
1425+
"experimentTemplateHistoryId": 22311,
1426+
"experimentTemplateId": 186,
1427+
"experimentTypeId": 1,
1428+
"handle": "es47og38wzhnuo",
1429+
"id": 22270,
1430+
"projectHandle": "przhbct98",
1431+
"projectId": 649,
1432+
"started_by_user_id": 1655,
1433+
"state": 7,
1434+
"templateHistory": {
1435+
"dtCreated": "2019-04-24T10:18:30.523193+00:00",
1436+
"dtDeleted": None,
1437+
"experimentTemplateId": 186,
1438+
"id": 22311,
1439+
"params": {
1440+
"command": ". test.sh\npython2 hello.py",
1441+
"container": "paperspace/tensorflow-python",
1442+
"machineType": "G1",
1443+
"project": "paperspace-python",
1444+
"workspaceFileName": "temp.zip"
1445+
},
1446+
"triggerEvent": None,
1447+
"triggerEventId": None
1448+
}
1449+
}
1450+
],
1451+
"meta": {
1452+
"itemGroup": {
1453+
"key": "projectHandle",
1454+
"value": "przhbct98"
1455+
},
1456+
"totalItems": 1
1457+
}
1458+
}
1459+
}
1460+
],
1461+
"meta": {
1462+
"totalItems": 3
1463+
}
1464+
}

tests/functional/test_projects.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import mock
2+
from click.testing import CliRunner
3+
4+
import paperspace
5+
from paperspace.cli import cli
6+
from paperspace.client import default_headers
7+
from tests import example_responses, MockResponse
8+
9+
10+
class TestListProjects(object):
11+
URL = "https://api.paperspace.io/projects/"
12+
EXPECTED_HEADERS = default_headers.copy()
13+
BASIC_COMMAND = ["projects", "list"]
14+
# TODO: change to `REQUEST_JSON = None` or whatever works when PS_API is fixed
15+
REQUEST_JSON = {'teamId': 666}
16+
EXPECTED_RESPONSE_JSON = example_responses.LIST_PROJECTS_RESPONSE
17+
EXPECTED_STDOUT = """+-----------+-------------------+------------+
18+
| ID | Name | Repository |
19+
+-----------+-------------------+------------+
20+
| prq70zy79 | test_project | None |
21+
| prmr22ve0 | keton | None |
22+
| przhbct98 | paperspace-python | None |
23+
+-----------+-------------------+------------+
24+
"""
25+
26+
BASIC_COMMAND_WITH_API_KEY = ["projects", "list", "--apiKey", "some_key"]
27+
EXPECTED_HEADERS_WITH_CHANGED_API_KEY = paperspace.client.default_headers.copy()
28+
EXPECTED_HEADERS_WITH_CHANGED_API_KEY["X-API-Key"] = "some_key"
29+
30+
RESPONSE_JSON_WITH_WRONG_API_TOKEN = {"status": 400, "message": "Invalid API token"}
31+
EXPECTED_STDOUT_WITH_WRONG_API_TOKEN = "Invalid API token\n"
32+
33+
RESPONSE_JSON_WHEN_NO_PROJECTS_WERE_FOUND = {"data": [], "meta": {"totalItems": 0}}
34+
EXPECTED_STDOUT_WHEN_NO_PROJECTS_WERE_FOUND = "No projects found\n"
35+
36+
@mock.patch("paperspace.cli.cli.client.requests.get")
37+
def test_should_send_valid_post_request_and_print_table_when_projects_list_was_used(self, get_patched):
38+
get_patched.return_value = MockResponse(json_data=self.EXPECTED_RESPONSE_JSON, status_code=200)
39+
40+
cli_runner = CliRunner()
41+
result = cli_runner.invoke(cli.cli, self.BASIC_COMMAND)
42+
43+
get_patched.assert_called_with(self.URL,
44+
headers=self.EXPECTED_HEADERS,
45+
json=self.REQUEST_JSON,
46+
params=None)
47+
assert result.output == self.EXPECTED_STDOUT
48+
assert result.exit_code == 0
49+
50+
@mock.patch("paperspace.cli.cli.client.requests.get")
51+
def test_should_send_valid_post_request_when_projects_list_was_used_with_api_key_option(self, get_patched):
52+
get_patched.return_value = MockResponse(json_data=self.EXPECTED_RESPONSE_JSON, status_code=200)
53+
54+
cli_runner = CliRunner()
55+
result = cli_runner.invoke(cli.cli, self.BASIC_COMMAND_WITH_API_KEY)
56+
57+
get_patched.assert_called_with(self.URL,
58+
headers=self.EXPECTED_HEADERS_WITH_CHANGED_API_KEY,
59+
json=self.REQUEST_JSON,
60+
params=None)
61+
assert result.output == self.EXPECTED_STDOUT
62+
assert result.exit_code == 0
63+
64+
@mock.patch("paperspace.cli.cli.client.requests.get")
65+
def test_should_send_valid_post_request_when_projects_list_was_used_with_wrong_api_key(self, get_patched):
66+
get_patched.return_value = MockResponse(json_data=self.RESPONSE_JSON_WITH_WRONG_API_TOKEN, status_code=400)
67+
68+
cli_runner = CliRunner()
69+
result = cli_runner.invoke(cli.cli, self.BASIC_COMMAND_WITH_API_KEY)
70+
71+
get_patched.assert_called_with(self.URL,
72+
headers=self.EXPECTED_HEADERS_WITH_CHANGED_API_KEY,
73+
json=self.REQUEST_JSON,
74+
params=None)
75+
assert result.output == self.EXPECTED_STDOUT_WITH_WRONG_API_TOKEN
76+
assert result.exit_code == 0
77+
78+
@mock.patch("paperspace.cli.cli.client.requests.get")
79+
def test_should_print_error_message_when_no_project_was_not_found(self, get_patched):
80+
get_patched.return_value = MockResponse(json_data=self.RESPONSE_JSON_WHEN_NO_PROJECTS_WERE_FOUND,
81+
status_code=200)
82+
83+
cli_runner = CliRunner()
84+
result = cli_runner.invoke(cli.cli, self.BASIC_COMMAND)
85+
86+
get_patched.assert_called_with(self.URL,
87+
headers=self.EXPECTED_HEADERS,
88+
json=self.REQUEST_JSON,
89+
params=None)
90+
assert result.output == self.EXPECTED_STDOUT_WHEN_NO_PROJECTS_WERE_FOUND
91+
assert result.exit_code == 0
92+
93+
@mock.patch("paperspace.cli.cli.client.requests.get")
94+
def test_should_print_error_message_when_error_status_code_received_but_no_content_was_provided(self, get_patched):
95+
get_patched.return_value = MockResponse(status_code=400)
96+
97+
cli_runner = CliRunner()
98+
result = cli_runner.invoke(cli.cli, self.BASIC_COMMAND)
99+
100+
get_patched.assert_called_with(self.URL,
101+
headers=self.EXPECTED_HEADERS,
102+
json=self.REQUEST_JSON,
103+
params=None)
104+
assert result.output == "Error while parsing response data: No JSON\n"
105+
assert result.exit_code == 0

0 commit comments

Comments
 (0)