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

Commit c8c0684

Browse files
committed
run command, restoring default files upload on job creation, restoring default values for project and container
1 parent 765ebe5 commit c8c0684

File tree

14 files changed

+377
-144
lines changed

14 files changed

+377
-144
lines changed

paperspace/cli/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import paperspace.cli.machines
1111
import paperspace.cli.models
1212
import paperspace.cli.projects
13+
import paperspace.cli.run
1314

1415

1516
def show(self, file=None):

paperspace/cli/jobs.py

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import functools
2+
13
import click
24

35
from paperspace import client, config
@@ -64,28 +66,35 @@ def list_jobs(api_key, **filters):
6466
command.execute(filters=filters)
6567

6668

69+
def common_jobs_create_options(f):
70+
options = [
71+
click.option("--name", "name", help="Job name", required=True),
72+
click.option("--machineType", "machineType", help="Virtual machine type"),
73+
click.option("--container", "container", default="paperspace/tensorflow-python", help="Docker container"),
74+
click.option("--command", "command", help="Job command/entrypoint"),
75+
click.option("--ports", "ports", help="Mapped ports"),
76+
click.option("--isPublic", "isPublic", help="Flag: is job public"),
77+
click.option("--workspace", "workspace", required=False, help="Path to workspace directory"),
78+
click.option("--workspaceArchive", "workspaceArchive", required=False, help="Path to workspace archive"),
79+
click.option("--workspaceUrl", "workspaceUrl", required=False, help="Project git repository url"),
80+
click.option("--workingDirectory", "workingDirectory", help="Working directory for the experiment", ),
81+
click.option("--ignoreFiles", "ignore_files", help="Ignore certain files from uploading"),
82+
click.option("--experimentId", "experimentId", help="Experiment Id"),
83+
click.option("--jobEnv", "envVars", type=json_string, help="Environmental variables "),
84+
click.option("--useDockerfile", "useDockerfile", help="Flag: using Dockerfile"),
85+
click.option("--isPreemptible", "isPreemptible", help="Flag: isPreemptible"),
86+
click.option("--project", "project", help="Project name"),
87+
click.option("--projectId", "projectHandle", help="Project ID"),
88+
click.option("--startedByUserId", "startedByUserId", help="User ID"),
89+
click.option("--relDockerfilePath", "relDockerfilePath", help="Relative path to Dockerfile"),
90+
click.option("--registryUsername", "registryUsername", help="Docker registry username"),
91+
click.option("--registryPassword", "registryPassword", help="Docker registry password"),
92+
]
93+
return functools.reduce(lambda x, opt: opt(x), reversed(options), f)
94+
95+
6796
@jobs_group.command("create", help="Create job")
68-
@click.option("--name", "name", help="Job name", required=True)
69-
@click.option("--machineType", "machineType", help="Virtual machine type")
70-
@click.option("--container", "container", help="Docker container")
71-
@click.option("--command", "command", help="Job command/entrypoint")
72-
@click.option("--ports", "ports", help="Mapped ports")
73-
@click.option("--isPublic", "isPublic", help="Flag: is job public")
74-
@click.option("--workspace", "workspace", required=False, help="Path to workspace directory")
75-
@click.option("--workspaceArchive", "workspaceArchive", required=False, help="Path to workspace archive")
76-
@click.option("--workspaceUrl", "workspaceUrl", required=False, help="Project git repository url")
77-
@click.option("--workingDirectory", "workingDirectory", help="Working directory for the experiment")
78-
@click.option("--ignoreFiles", "ignore_files", help="Ignore certain files from uploading")
79-
@click.option("--experimentId", "experimentId", help="Experiment Id")
80-
@click.option("--jobEnv", "envVars", type=json_string, help="Environmental variables ")
81-
@click.option("--useDockerfile", "useDockerfile", help="Flag: using Dockerfile")
82-
@click.option("--isPreemptible", "isPreemptible", help="Flag: isPreemptible")
83-
@click.option("--project", "project", help="Project name")
84-
@click.option("--projectId", "projectHandle", help="Project ID", required=True)
85-
@click.option("--startedByUserId", "startedByUserId", help="User ID")
86-
@click.option("--relDockerfilePath", "relDockerfilePath", help="Relative path to Dockerfile")
87-
@click.option("--registryUsername", "registryUsername", help="Docker registry username")
88-
@click.option("--registryPassword", "registryPassword", help="Docker registry password")
97+
@common_jobs_create_options
8998
@api_key_option
9099
def create_job(api_key, **kwargs):
91100
del_if_value_is_none(kwargs)

paperspace/cli/run.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import os
2+
import sys
3+
4+
import click
5+
6+
from paperspace import client, config
7+
from paperspace.cli import common
8+
from paperspace.cli.cli import cli
9+
from paperspace.cli.common import del_if_value_is_none
10+
from paperspace.cli.jobs import common_jobs_create_options
11+
from paperspace.commands.jobs import CreateJobCommand
12+
from paperspace.workspace import WorkspaceHandler
13+
14+
RUN_MODE_DEFAULT = 1
15+
RUN_MODE_PYTHON_COMMAND = 2
16+
RUN_MODE_SHELL_COMMAND = 3
17+
RUN_MODE_PYTHON_MODULE = 4
18+
19+
20+
def get_executor(mode, python_version=None):
21+
python_version = python_version or str(sys.version_info[0]) # defaults locally running version
22+
python_bin = 'python{v}'.format(v=python_version)
23+
executors = {
24+
RUN_MODE_DEFAULT: python_bin,
25+
RUN_MODE_PYTHON_COMMAND: '{python} -c'.format(python=python_bin),
26+
RUN_MODE_SHELL_COMMAND: '',
27+
RUN_MODE_PYTHON_MODULE: '{python} -m'.format(python=python_bin),
28+
}
29+
return executors[mode]
30+
31+
32+
def clear_script_name(script_name, mode):
33+
if mode == RUN_MODE_DEFAULT:
34+
return os.path.basename(script_name)
35+
return script_name
36+
37+
38+
def create_command(mode, script, python_version=None):
39+
executor = get_executor(mode, python_version)
40+
script_name = clear_script_name(script[0], mode)
41+
script_params = ' '.join(script[1:])
42+
command = '{executor} {script_name} {script_params}'.format(executor=executor, script_name=script_name,
43+
script_params=script_params)
44+
return command
45+
46+
47+
@cli.command("new-run")
48+
@click.option("-c", "--python-command", "mode", flag_value=RUN_MODE_PYTHON_COMMAND)
49+
@click.option("-m", "--module", "mode", flag_value=RUN_MODE_PYTHON_MODULE)
50+
@click.option("-s", "--shell", "mode", flag_value=RUN_MODE_SHELL_COMMAND)
51+
@common_jobs_create_options
52+
@click.argument("script", nargs=-1)
53+
@common.api_key_option
54+
def run(script, api_key, mode, **kwargs):
55+
del_if_value_is_none(kwargs)
56+
57+
mode = mode or RUN_MODE_DEFAULT
58+
command = create_command(mode, script)
59+
kwargs['command'] = command
60+
61+
jobs_api = client.API(config.CONFIG_HOST, api_key=api_key)
62+
command = CreateJobCommand(api=jobs_api, workspace_handler=WorkspaceHandler())
63+
command.execute(kwargs)

paperspace/client.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ def get_path(self, url):
2828
template = "{}{}" if url.startswith("/") else "{}/{}"
2929
return template.format(api_url, url)
3030

31-
def post(self, url, json=None, params=None, files=None):
31+
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: {}"
34-
.format(path, self.headers, json, params))
35-
response = requests.post(path, json=json, params=params, headers=self.headers, files=files)
33+
logger.debug("POST request sent to: {} \n\theaders: {}\n\tjson: {}\n\tparams: {}\n\tdata: {}"
34+
.format(path, self.headers, json, params, data))
35+
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))
3838
return response

paperspace/commands/experiments.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def _log_create_experiment(self, response, success_msg_template, error_msg):
3030
class CreateExperimentCommand(ExperimentCommand):
3131

3232
def execute(self, json_):
33-
workspace_url = self._workspace_handler.upload_workspace(json_)
33+
workspace_url = self._workspace_handler.handle(json_)
3434
if workspace_url:
3535
json_['workspaceUrl'] = workspace_url
3636

@@ -43,7 +43,7 @@ def execute(self, json_):
4343

4444
class CreateAndStartExperimentCommand(ExperimentCommand):
4545
def execute(self, json_):
46-
workspace_url = self._workspace_handler.upload_workspace(json_)
46+
workspace_url = self._workspace_handler.handle(json_)
4747
if workspace_url:
4848
json_['workspaceUrl'] = workspace_url
4949

paperspace/commands/jobs.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from paperspace.commands import common
88
from paperspace.exceptions import BadResponseError
99
from paperspace.utils import get_terminal_lines
10-
from paperspace.workspace import S3WorkspaceHandler
10+
from paperspace.workspace import S3WorkspaceHandler, WorkspaceHandler
1111

1212

1313
class JobsCommandBase(common.CommandBase):
@@ -126,22 +126,43 @@ def _make_table(self, logs, table, table_data):
126126
class CreateJobCommand(JobsCommandBase):
127127
def __init__(self, workspace_handler=None, **kwargs):
128128
super(CreateJobCommand, self).__init__(**kwargs)
129-
experiments_api = client.API(config.CONFIG_EXPERIMENTS_HOST, api_key=kwargs.get('api_key'))
130-
self._workspace_handler = workspace_handler or S3WorkspaceHandler(experiments_api=experiments_api,
131-
logger=self.logger)
129+
# experiments_api = client.API(config.CONFIG_EXPERIMENTS_HOST, api_key=kwargs.get('api_key'))
130+
self._workspace_handler = workspace_handler or WorkspaceHandler(logger=self.logger)
132131

133132
def execute(self, json_):
134133
url = "/jobs/createJob/"
135-
136-
workspace_url = self._workspace_handler.upload_workspace(json_)
134+
files = None
135+
workspace_url = self._workspace_handler.handle(json_)
137136
if workspace_url:
138-
json_['workspaceFileName'] = workspace_url
139-
json_['projectId'] = json_.get('projectId', json_.get('projectHandle'))
140-
response = self.api.post(url, json_)
137+
if self._workspace_handler.archive_path:
138+
archive_basename = self._workspace_handler.archive_basename
139+
json_["workspaceFileName"] = archive_basename
140+
# json_["file"] = open(workspace_url, "rb")
141+
# monitor = MultipartEncoder(json_).get_monitor()
142+
# # s3_encoder = encoder.MultipartEncoder(fields={"file": open(workspace_url, "rb")})
143+
# # monitor = encoder.MultipartEncoderMonitor(s3_encoder, callback=self._create_callback(s3_encoder))
144+
# self.api.headers["Content-Type"] = monitor.content_type
145+
self.api.headers["Content-Type"] = "multipart/form-data"
146+
# data = monitor
147+
files = {"file": open(workspace_url, "rb")}
148+
else:
149+
json_["workspaceFileName"] = workspace_url
150+
151+
self.set_project(json_)
152+
153+
response = self.api.post(url, params=json_, files=files)
141154
self._log_message(response,
142155
"Job created",
143156
"Unknown error while creating job")
144157

158+
@staticmethod
159+
def set_project(json_):
160+
project_id = json_.get("projectId", json_.get("projectHandle"))
161+
if not project_id:
162+
json_["project"] = "paperspace-python"
163+
else:
164+
json_["projectId"] = project_id
165+
145166

146167
class ArtifactsDestroyCommand(JobsCommandBase):
147168
def execute(self, job_id, files=None):

paperspace/workspace.py

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,34 @@
1313
PresignedUrlMalformedResponseError, PresignedUrlError
1414

1515

16-
class S3WorkspaceHandler:
17-
def __init__(self, experiments_api, logger=None):
16+
class MultipartEncoder(object):
17+
def __init__(self, fields):
18+
s3_encoder = encoder.MultipartEncoder(fields=fields)
19+
self.monitor = encoder.MultipartEncoderMonitor(s3_encoder, callback=self._create_callback(s3_encoder))
20+
21+
def get_monitor(self):
22+
return self.monitor
23+
24+
@staticmethod
25+
def _create_callback(encoder_obj):
26+
bar = progressbar.ProgressBar(max_value=encoder_obj.len)
27+
28+
def callback(monitor):
29+
bar.update(monitor.bytes_read)
30+
31+
return callback
32+
33+
34+
class WorkspaceHandler(object):
35+
def __init__(self, logger=None):
1836
"""
1937
2038
:param experiments_api: paperspace.client.API
2139
:param logger: paperspace.logger
2240
"""
23-
self.experiments_api = experiments_api
2441
self.logger = logger or default_logger
42+
self.archive_path = None
43+
self.archive_basename = None
2544

2645
@staticmethod
2746
def _retrieve_file_paths(dir_name, ignored_files=None):
@@ -77,27 +96,49 @@ def _zip_workspace(self, workspace_path, ignore_files):
7796
self.logger.log('\nFinished creating archive: %s' % zip_file_name)
7897
return zip_file_path
7998

80-
@staticmethod
81-
def _create_callback(encoder_obj):
82-
bar = progressbar.ProgressBar(max_value=encoder_obj.len)
99+
def handle(self, input_data):
100+
workspace_archive, workspace_path, workspace_url = self._validate_input(input_data)
101+
ignore_files = input_data.get('ignore_files')
83102

84-
def callback(monitor):
85-
bar.update(monitor.bytes_read)
103+
if workspace_url:
104+
return # nothing to do
86105

87-
return callback
106+
if workspace_archive:
107+
archive_path = os.path.abspath(workspace_archive)
108+
else:
109+
archive_path = self._zip_workspace(workspace_path, ignore_files)
110+
self.archive_path = archive_path
111+
self.archive_basename = os.path.basename(archive_path)
112+
return archive_path
88113

89-
def upload_workspace(self, input_data):
114+
@staticmethod
115+
def _validate_input(input_data):
90116
workspace_url = input_data.get('workspaceUrl')
91117
workspace_path = input_data.get('workspace')
92118
workspace_archive = input_data.get('workspaceArchive')
93-
ignore_files = input_data.get('ignore_files')
94119

95120
if (workspace_archive and workspace_path) or (workspace_archive and workspace_url) or (
96121
workspace_path and workspace_url):
97122
raise click.UsageError("Use either:\n\t--workspaceUrl to point repository URL"
98123
"\n\t--workspace to point on project directory"
99124
"\n\t--workspaceArchive to point on project .zip archive"
100125
"\n or neither to use current directory")
126+
return workspace_archive, workspace_path, workspace_url
127+
128+
129+
class S3WorkspaceHandler(WorkspaceHandler):
130+
def __init__(self, experiments_api, logger=None):
131+
"""
132+
133+
:param experiments_api: paperspace.client.API
134+
:param logger: paperspace.logger
135+
"""
136+
super(S3WorkspaceHandler, self).__init__(logger=logger)
137+
self.experiments_api = experiments_api
138+
139+
def handle(self, input_data):
140+
workspace_archive, workspace_path, workspace_url = self._validate_input(input_data)
141+
ignore_files = input_data.get('ignore_files')
101142

102143
if workspace_url:
103144
return # nothing to do
@@ -133,8 +174,7 @@ def _upload(self, archive_path, s3_upload_data):
133174
fields = OrderedDict(s3_upload_data['fields'])
134175
fields.update(files)
135176

136-
s3_encoder = encoder.MultipartEncoder(fields=fields)
137-
monitor = encoder.MultipartEncoderMonitor(s3_encoder, callback=self._create_callback(s3_encoder))
177+
monitor = MultipartEncoder(fields).get_monitor()
138178
s3_response = requests.post(s3_upload_data['url'], data=monitor, headers={'Content-Type': monitor.content_type})
139179
self.logger.debug("S3 upload response: {}".format(s3_response.headers))
140180
if not s3_response.ok:

0 commit comments

Comments
 (0)