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

Commit 4eee6e6

Browse files
Merge pull request #115 from Paperspace/send-workspace-with-raw-post
Send workspace with raw post
2 parents 4002a53 + 300bfae commit 4eee6e6

File tree

6 files changed

+127
-36
lines changed

6 files changed

+127
-36
lines changed

paperspace/cli/jobs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def common_jobs_create_options(f):
8484
click.option("--useDockerfile", "useDockerfile", help="Flag: using Dockerfile"),
8585
click.option("--isPreemptible", "isPreemptible", help="Flag: isPreemptible"),
8686
click.option("--project", "project", help="Project name"),
87-
click.option("--projectId", "projectHandle", help="Project ID"),
87+
click.option("--projectId", "projectId", help="Project ID"),
8888
click.option("--startedByUserId", "startedByUserId", help="User ID"),
8989
click.option("--relDockerfilePath", "relDockerfilePath", help="Relative path to Dockerfile"),
9090
click.option("--registryUsername", "registryUsername", help="Docker registry username"),

paperspace/client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ def get_path(self, url):
3030

3131
def post(self, url, json=None, params=None, files=None, data=None):
3232
path = self.get_path(url)
33-
logger.debug("POST request sent to: {} \n\theaders: {}\n\tjson: {}\n\tparams: {}\n\tdata: {}"
34-
.format(path, self.headers, json, params, data))
33+
logger.debug("POST request sent to: {} \n\theaders: {}\n\tjson: {}\n\tparams: {}\n\tfiles: {}\n\tdata: {}"
34+
.format(path, self.headers, json, params, files, data))
3535
response = requests.post(path, json=json, params=params, headers=self.headers, files=files, data=data)
3636
logger.debug("Response status code: {}".format(response.status_code))
3737
logger.debug("Response content: {}".format(response.content))

paperspace/commands/jobs.py

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from paperspace.commands import common
77
from paperspace.exceptions import BadResponseError
88
from paperspace.utils import get_terminal_lines
9-
from paperspace.workspace import WorkspaceHandler
9+
from paperspace.workspace import WorkspaceHandler, MultipartEncoder
1010

1111

1212
class JobsCommandBase(common.CommandBase):
@@ -108,7 +108,7 @@ def _get_logs(self, job_id, line, limit):
108108
'jobId': job_id,
109109
'line': line,
110110
'limit': limit
111-
};
111+
}
112112
return self.api.get(self.base_url, params=params)
113113

114114
def _log_logs_list(self, data, table, table_data, follow):
@@ -149,36 +149,39 @@ def __init__(self, workspace_handler=None, **kwargs):
149149

150150
def execute(self, json_):
151151
url = "/jobs/createJob/"
152-
files = None
152+
data = None
153+
self.set_project_if_not_provided(json_)
154+
153155
workspace_url = self._workspace_handler.handle(json_)
154156
if workspace_url:
155157
if self._workspace_handler.archive_path:
156-
archive_basename = self._workspace_handler.archive_basename
157-
json_["workspaceFileName"] = archive_basename
158-
self.api.headers["Content-Type"] = "multipart/form-data"
159-
files = self._get_files_dict(workspace_url)
158+
data = self._get_multipart_data(json_)
160159
else:
161160
json_["workspaceFileName"] = workspace_url
162161

163-
self.set_project(json_)
164-
165-
response = self.api.post(url, params=json_, files=files)
162+
self.logger.log("Creating job...")
163+
response = self.api.post(url, params=json_, data=data)
166164
self._log_message(response,
167-
"Job created",
165+
"Job created - ID: {id}",
168166
"Unknown error while creating job")
169167

170-
@staticmethod
171-
def _get_files_dict(workspace_url):
172-
files = {"file": open(workspace_url, "rb")}
173-
return files
168+
def _get_multipart_data(self, json_):
169+
archive_basename = self._workspace_handler.archive_basename
170+
json_["workspaceFileName"] = archive_basename
171+
job_data = self._get_files_dict(archive_basename)
172+
monitor = MultipartEncoder(job_data).get_monitor()
173+
self.api.headers["Content-Type"] = monitor.content_type
174+
data = monitor
175+
return data
176+
177+
def _get_files_dict(self, archive_basename):
178+
job_data = {'file': (archive_basename, open(self._workspace_handler.archive_path, 'rb'), 'text/plain')}
179+
return job_data
174180

175181
@staticmethod
176-
def set_project(json_):
177-
project_id = json_.get("projectId", json_.get("projectHandle"))
178-
if not project_id:
182+
def set_project_if_not_provided(json_):
183+
if not json_.get("projectId"):
179184
json_["project"] = "paperspace-python"
180-
else:
181-
json_["projectId"] = project_id
182185

183186

184187
class ArtifactsDestroyCommand(JobsCommandBase):

paperspace/workspace.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515

1616
class MultipartEncoder(object):
1717
def __init__(self, fields):
18-
s3_encoder = encoder.MultipartEncoder(fields=fields)
19-
self.monitor = encoder.MultipartEncoderMonitor(s3_encoder, callback=self._create_callback(s3_encoder))
18+
mp_encoder = encoder.MultipartEncoder(fields=fields)
19+
self.monitor = encoder.MultipartEncoderMonitor(mp_encoder, callback=self._create_callback(mp_encoder))
2020

2121
def get_monitor(self):
2222
return self.monitor
@@ -26,7 +26,10 @@ def _create_callback(encoder_obj):
2626
bar = progressbar.ProgressBar(max_value=encoder_obj.len)
2727

2828
def callback(monitor):
29-
bar.update(monitor.bytes_read)
29+
if monitor.bytes_read == bar.max_value:
30+
bar.finish()
31+
else:
32+
bar.update(monitor.bytes_read)
3033

3134
return callback
3235

@@ -105,13 +108,12 @@ def handle(self, input_data):
105108

106109
# Should be removed as soon it won't be necessary by PS_API
107110
if workspace_path == 'none':
108-
return workspace_path
109-
111+
return 'none'
110112
if workspace_archive:
111113
archive_path = os.path.abspath(workspace_archive)
112114
else:
113115
self.logger.log('Archiving your working directory for upload as your experiment workspace...'
114-
'(See https://docs.paperspace.com/gradient/experiments/run-experiments for more information.)')
116+
'(See https://docs.paperspace.com/gradient/experiments/run-experiments for more information.)')
115117
archive_path = self._zip_workspace(workspace_path, ignore_files)
116118
self.archive_path = archive_path
117119
self.archive_basename = os.path.basename(archive_path)

tests/functional/test_jobs.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,3 +366,86 @@ def test_should_send_valid_get_request_with_valid_param_for_a_list_of_artifacts_
366366
params={"jobId": job_id,
367367
param: True})
368368
assert result.exit_code == 0
369+
370+
371+
class TestJobsCreate(object):
372+
URL = "https://api.paperspace.io"
373+
EXPECTED_HEADERS = default_headers.copy()
374+
EXPECTED_HEADERS_WITH_CHANGED_API_KEY = default_headers.copy()
375+
EXPECTED_HEADERS_WITH_CHANGED_API_KEY["X-API-Key"] = "some_key"
376+
BASIC_OPTIONS_COMMAND = [
377+
"jobs", "create",
378+
"--name", "exp1",
379+
"--projectId", "testHandle",
380+
"--container", "testContainer",
381+
"--machineType", "testType",
382+
"--command", "testCommand",
383+
"--workspaceUrl", "some-workspace",
384+
]
385+
FULL_OPTIONS_COMMAND = [
386+
"jobs", "create",
387+
"--name", "exp1",
388+
"--ports", 4567,
389+
"--workspaceUrl", "wsp.url",
390+
"--workingDirectory", "/work/dir/",
391+
"--artifactDirectory", "/artifact/dir/",
392+
"--clusterId", 42,
393+
"--experimentEnv", '{"key":"val"}',
394+
"--projectId", "testHandle",
395+
"--container", "testContainer",
396+
"--machineType", "testType",
397+
"--command", "testCommand",
398+
"--containerUser", "conUser",
399+
"--registryUsername", "userName",
400+
"--registryPassword", "passwd",
401+
"--apiKey", "some_key",
402+
]
403+
BASIC_OPTIONS_REQUEST = {
404+
"name": u"exp1",
405+
"projectId": u"testHandle",
406+
"container": u"testContainer",
407+
"machineType": u"testType",
408+
"command": u"testCommand",
409+
"workspaceUrl": u"some-workspace",
410+
"workspaceFileName": u"some-workspace",
411+
}
412+
FULL_OPTIONS_REQUEST = {
413+
"name": u"exp1",
414+
"ports": 4567,
415+
"workspaceUrl": u"wsp.url",
416+
"workingDirectory": u"/work/dir/",
417+
"artifactDirectory": u"/artifact/dir/",
418+
"clusterId": 42,
419+
"experimentEnv": {u"key": u"val"},
420+
"projectHandle": u"testHandle",
421+
"container": u"testContainer",
422+
"machineType": u"testType",
423+
"command": u"testCommand",
424+
"containerUser": u"conUser",
425+
"registryUsername": u"userName",
426+
"registryPassword": u"passwd",
427+
}
428+
RESPONSE_JSON_200 = {"id": "sadkfhlskdjh", "message": "success"}
429+
RESPONSE_CONTENT_200 = b'{"handle":"sadkfhlskdjh","message":"success"}\n'
430+
EXPECTED_STDOUT = u'Creating job...\nJob created - ID: sadkfhlskdjh\n'
431+
432+
RESPONSE_JSON_404_PROJECT_NOT_FOUND = {"details": {"handle": "wrong_handle"}, "error": "Project not found"}
433+
RESPONSE_CONTENT_404_PROJECT_NOT_FOUND = b'{"details":{"handle":"wrong_handle"},"error":"Project not found"}\n'
434+
EXPECTED_STDOUT_PROJECT_NOT_FOUND = "Project not found\nhandle: wrong_handle\n"
435+
436+
@mock.patch("paperspace.client.requests.post")
437+
def test_should_send_proper_data_and_print_message_when_create_job_was_run_with_basic_options(self, post_patched):
438+
post_patched.return_value = MockResponse(self.RESPONSE_JSON_200, 200, self.RESPONSE_CONTENT_200)
439+
440+
runner = CliRunner()
441+
result = runner.invoke(cli.cli, self.BASIC_OPTIONS_COMMAND)
442+
443+
post_patched.assert_called_once_with(self.URL + '/jobs/createJob/',
444+
headers=self.EXPECTED_HEADERS,
445+
json=None,
446+
params=self.BASIC_OPTIONS_REQUEST,
447+
files=None,
448+
data=None)
449+
450+
assert result.output == self.EXPECTED_STDOUT
451+
assert result.exit_code == 0

tests/functional/test_run.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,32 @@ class TestRunCommand(object):
1717

1818
@mock.patch("paperspace.client.requests.post")
1919
@mock.patch("paperspace.workspace.WorkspaceHandler._zip_workspace")
20+
@mock.patch("paperspace.workspace.MultipartEncoder.get_monitor")
2021
@mock.patch("paperspace.commands.jobs.CreateJobCommand._get_files_dict")
21-
def test_run_simple_file_with_args(self, get_files_patched, workspace_zip_patched, post_patched):
22+
def test_run_simple_file_with_args(self, get_files_patched, get_moniror_patched, workspace_zip_patched, post_patched):
2223
get_files_patched.return_value = mock.MagicMock()
2324
workspace_zip_patched.return_value = '/foo/bar'
2425
post_patched.return_value = MockResponse(status_code=200)
2526

27+
mock_monitor = mock.MagicMock()
28+
mock_monitor.content_type = "mock/multipart"
29+
30+
get_moniror_patched.return_value = mock_monitor
31+
2632
runner = CliRunner()
2733
result = runner.invoke(cli.cli, [self.command_name] + self.common_commands + ["/myscript.py", "a", "b"])
2834

2935
expected_headers = self.headers.copy()
3036
expected_headers.update({
31-
'Content-Type': "multipart/form-data"
37+
'Content-Type': "mock/multipart"
3238
})
3339
post_patched.assert_called_with(self.url,
3440
params={'name': u'test', 'projectId': u'projectId',
3541
'workspaceFileName': 'bar',
3642
'command': 'python{} myscript.py a b'.format(str(sys.version_info[0])),
37-
'projectHandle': u'projectId',
3843
'container': u'paperspace/tensorflow-python'},
39-
data=None,
40-
files=mock.ANY,
44+
data=mock.ANY,
45+
files=None,
4146
headers=expected_headers,
4247
json=None)
4348

@@ -55,7 +60,6 @@ def test_run_python_command_with_args_and_no_workspace(self, post_patched):
5560
'workspaceFileName': 'none',
5661
'workspace': 'none',
5762
'command': 'python{} -c print(foo)'.format(str(sys.version_info[0])),
58-
'projectHandle': u'projectId',
5963
'container': u'paperspace/tensorflow-python'},
6064
data=None,
6165
files=None,
@@ -79,7 +83,6 @@ def test_run_shell_command_with_args_with_s3_workspace(self, workspace_zip_patch
7983
'workspaceFileName': 's3://bucket/object',
8084
'workspaceUrl': 's3://bucket/object',
8185
'command': 'echo foo',
82-
'projectHandle': u'projectId',
8386
'container': u'paperspace/tensorflow-python'},
8487
data=None,
8588
files=None,

0 commit comments

Comments
 (0)