Skip to content

Commit ffe9ad1

Browse files
authored
Merge pull request #34 from Paperspace/PS-10523-deprecate-workspaceUrl-and-workspacePath-and-let-the-workspace-do-their-job
Allow passing URLs and archive paths to --workspace and deprcecate --…
2 parents e7e0b8a + ca1aa80 commit ffe9ad1

File tree

11 files changed

+229
-35
lines changed

11 files changed

+229
-35
lines changed

gradient/cli/common.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,11 @@ class ClickGroup(DYMMixin, HelpColorsGroup):
3434
pass
3535

3636

37-
def deprecated(version="1.0.0"):
38-
deprecated_invoke_notice = """DeprecatedWarning: \nWARNING: This command will not be included in version %s .
39-
For more information, please see:
37+
def deprecated(msg):
38+
deprecated_invoke_notice = msg + """\nFor more information, please see:
4039
4140
https://docs.paperspace.com
42-
If you depend on functionality not listed there, please file an issue.""" % version
41+
If you depend on functionality not listed there, please file an issue."""
4342

4443
def new_invoke(self, ctx):
4544
click.echo(click.style(deprecated_invoke_notice, fg='red'), err=True)

gradient/cli/experiments.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33

44
import click
55

6-
from gradient import client, config, constants
6+
from gradient import client, config, constants, utils
77
from gradient.cli.cli import cli
88
from gradient.cli.cli_types import json_string, ChoiceType
9-
from gradient.cli.common import api_key_option, del_if_value_is_none, ClickGroup
9+
from gradient.cli.common import api_key_option, del_if_value_is_none, ClickGroup, deprecated
1010
from gradient.commands import experiments as experiments_commands
1111

1212
MULTI_NODE_EXPERIMENT_TYPES_MAP = collections.OrderedDict(
@@ -235,27 +235,35 @@ def common_experiments_create_single_node_options(f):
235235
return functools.reduce(lambda x, opt: opt(x), reversed(options), f)
236236

237237

238+
@deprecated("DeprecatedWarning: \nWARNING: --workspaceUrl and --workspaceArchive "
239+
"options will not be included in version 0.6.0")
238240
@create_experiment.command(name="multinode", help="Create multi node experiment")
239241
@common_experiments_create_options
240242
@common_experiment_create_multi_node_options
241243
def create_multi_node(api_key, **kwargs):
244+
utils.validate_workspace_input(kwargs)
242245
del_if_value_is_none(kwargs)
243246
experiments_api = client.API(config.CONFIG_EXPERIMENTS_HOST, api_key=api_key)
244247
command = experiments_commands.CreateExperimentCommand(api=experiments_api)
245248
command.execute(kwargs)
246249

247250

251+
@deprecated("DeprecatedWarning: \nWARNING: --workspaceUrl and --workspaceArchive "
252+
"options will not be included in version 0.6.0")
248253
@create_experiment.command(name="singlenode", help="Create single node experiment")
249254
@common_experiments_create_options
250255
@common_experiments_create_single_node_options
251256
def create_single_node(api_key, **kwargs):
257+
utils.validate_workspace_input(kwargs)
252258
kwargs["experimentTypeId"] = constants.ExperimentType.SINGLE_NODE
253259
del_if_value_is_none(kwargs)
254260
experiments_api = client.API(config.CONFIG_EXPERIMENTS_HOST, api_key=api_key)
255261
command = experiments_commands.CreateExperimentCommand(api=experiments_api)
256262
command.execute(kwargs)
257263

258264

265+
@deprecated("DeprecatedWarning: \nWARNING: --workspaceUrl and --workspaceArchive "
266+
"options will not be included in version 0.6.0")
259267
@create_and_start_experiment.command(name="multinode", help="Create and start new multi node experiment")
260268
@common_experiments_create_options
261269
@common_experiment_create_multi_node_options
@@ -269,6 +277,7 @@ def create_single_node(api_key, **kwargs):
269277
)
270278
@click.pass_context
271279
def create_and_start_multi_node(ctx, api_key, show_logs, **kwargs):
280+
utils.validate_workspace_input(kwargs)
272281
del_if_value_is_none(kwargs)
273282
experiments_api = client.API(config.CONFIG_EXPERIMENTS_HOST, api_key=api_key)
274283
command = experiments_commands.CreateAndStartExperimentCommand(api=experiments_api)
@@ -277,6 +286,8 @@ def create_and_start_multi_node(ctx, api_key, show_logs, **kwargs):
277286
ctx.invoke(list_logs, experiment_id=experiment["handle"], line=0, limit=100, follow=True, api_key=api_key)
278287

279288

289+
@deprecated("DeprecatedWarning: \nWARNING: --workspaceUrl and --workspaceArchive "
290+
"options will not be included in version 0.6.0")
280291
@create_and_start_experiment.command(name="singlenode", help="Create and start new single node experiment")
281292
@common_experiments_create_options
282293
@common_experiments_create_single_node_options
@@ -290,6 +301,7 @@ def create_and_start_multi_node(ctx, api_key, show_logs, **kwargs):
290301
)
291302
@click.pass_context
292303
def create_and_start_single_node(ctx, api_key, show_logs, **kwargs):
304+
utils.validate_workspace_input(kwargs)
293305
kwargs["experimentTypeId"] = constants.ExperimentType.SINGLE_NODE
294306
del_if_value_is_none(kwargs)
295307
experiments_api = client.API(config.CONFIG_EXPERIMENTS_HOST, api_key=api_key)

gradient/cli/jobs.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
import click
44

5-
from gradient import client, config
5+
from gradient import client, config, utils
66
from gradient.cli.cli import cli
77
from gradient.cli.cli_types import json_string
8-
from gradient.cli.common import api_key_option, del_if_value_is_none, ClickGroup, jsonify_dicts
8+
from gradient.cli.common import api_key_option, del_if_value_is_none, ClickGroup, jsonify_dicts, deprecated
99
from gradient.commands import jobs as jobs_commands
1010

1111

@@ -96,11 +96,14 @@ def common_jobs_create_options(f):
9696
return functools.reduce(lambda x, opt: opt(x), reversed(options), f)
9797

9898

99+
@deprecated("DeprecatedWarning: \nWARNING: --workspaceUrl and --workspaceArchive "
100+
"options will not be included in version 0.6.0")
99101
@jobs_group.command("create", help="Create job")
100102
@common_jobs_create_options
101103
@api_key_option
102104
@click.pass_context
103105
def create_job(ctx, api_key, **kwargs):
106+
utils.validate_workspace_input(kwargs)
104107
del_if_value_is_none(kwargs)
105108
jsonify_dicts(kwargs)
106109

gradient/cli/run.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import click
22

3-
from gradient import client, config
3+
from gradient import client, config, utils
44
from gradient.cli import common
55
from gradient.cli.cli import cli
66
from gradient.cli.common import del_if_value_is_none, deprecated, jsonify_dicts
@@ -9,7 +9,9 @@
99
from gradient.constants import RunMode
1010

1111

12-
@deprecated(version="0.6.0")
12+
@deprecated("DeprecatedWarning: \nWARNING: This command will not be included in version 0.6.0\n"
13+
"DeprecatedWarning: \nWARNING: --workspaceUrl and --workspaceArchive "
14+
"options will not be included in version 0.6.0")
1315
@cli.command("run", help="Run script or command on remote cluster")
1416
@click.option("-c", "--python-command", "mode", flag_value=RunMode.RUN_MODE_PYTHON_COMMAND)
1517
@click.option("-m", "--module", "mode", flag_value=RunMode.RUN_MODE_PYTHON_MODULE)
@@ -18,6 +20,7 @@
1820
@click.argument("script", nargs=-1, required=True)
1921
@common.api_key_option
2022
def run(api_key, **kwargs):
23+
utils.validate_workspace_input(kwargs)
2124
del_if_value_is_none(kwargs)
2225
jsonify_dicts(kwargs)
2326

gradient/exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,11 @@ class PresignedUrlError(ApplicationError):
3232

3333
class S3UploadFailedError(ApplicationError):
3434
pass
35+
36+
37+
class WrongPathError(ApplicationError):
38+
pass
39+
40+
41+
class MutuallyExclusiveParametersUsedError(Exception):
42+
pass

gradient/utils.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import json
2+
import os
23
import shutil
34

5+
import click
46
import requests
57
import six
68

9+
from gradient import exceptions
10+
711

812
def get_terminal_lines(fallback=48):
913
if six.PY3:
@@ -32,4 +36,58 @@ def status_code_to_error_obj(status_code):
3236
message = 'unknown'
3337
if status_code in requests.status_codes._codes:
3438
message = requests.status_codes._codes[status_code][0]
35-
return { 'error': True, 'message': message, 'status': status_code }
39+
return { 'error': True, 'message': message, 'status': status_code }
40+
41+
42+
class PathParser(object):
43+
LOCAL_DIR = 0
44+
LOCAL_FILE = 1
45+
GIT_URL = 2
46+
S3_URL = 3
47+
48+
@classmethod
49+
def parse_path(cls, path):
50+
if cls.is_local_dir(path):
51+
return cls.LOCAL_DIR
52+
53+
if cls.is_local_zip_file(path):
54+
return cls.LOCAL_FILE
55+
56+
if cls.is_git_url(path):
57+
return cls.GIT_URL
58+
59+
if cls.is_s3_url(path):
60+
return cls.S3_URL
61+
62+
raise exceptions.WrongPathError("Given path is neither local path, nor valid URL")
63+
64+
@staticmethod
65+
def is_local_dir(path):
66+
return os.path.exists(path) and os.path.isdir(path)
67+
68+
@staticmethod
69+
def is_local_zip_file(path):
70+
return os.path.exists(path) and os.path.isfile(path) and path.endswith(".zip")
71+
72+
@staticmethod
73+
def is_git_url(path):
74+
return not os.path.exists(path) and path.endswith(".git") or path.lower().startswith("git:")
75+
76+
@staticmethod
77+
def is_s3_url(path):
78+
return not os.path.exists(path) and path.lower().startswith("s3:")
79+
80+
81+
def validate_workspace_input(input_data):
82+
workspace_url = input_data.get('workspaceUrl')
83+
workspace_path = input_data.get('workspace')
84+
workspace_archive = input_data.get('workspaceArchive')
85+
86+
if (workspace_archive and workspace_path) \
87+
or (workspace_archive and workspace_url) \
88+
or (workspace_path and workspace_url):
89+
raise click.UsageError("Use either:\n\t--workspace https://path.to/git/repository.git - to point repository URL"
90+
"\n\t--workspace /path/to/local/directory - to point on project directory"
91+
"\n\t--workspace /path/to/local/archive.zip - to point on project .zip archive"
92+
"\n\t--workspace none - to use no workspace"
93+
"\n or neither to use current directory")

gradient/workspace.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import requests
88
from requests_toolbelt.multipart import encoder
99

10-
from gradient import logger
10+
from gradient import logger, utils
1111
from gradient.exceptions import S3UploadFailedError, PresignedUrlUnreachableError, \
1212
PresignedUrlAccessDeniedError, PresignedUrlConnectionError, ProjectAccessDeniedError, \
1313
PresignedUrlMalformedResponseError, PresignedUrlError
@@ -120,16 +120,26 @@ def handle(self, input_data):
120120

121121
@staticmethod
122122
def _validate_input(input_data):
123+
utils.validate_workspace_input(input_data)
124+
123125
workspace_url = input_data.get('workspaceUrl')
124126
workspace_path = input_data.get('workspace')
125127
workspace_archive = input_data.get('workspaceArchive')
126128

127-
if (workspace_archive and workspace_path) or (workspace_archive and workspace_url) or (
128-
workspace_path and workspace_url):
129-
raise click.UsageError("Use either:\n\t--workspaceUrl to point repository URL"
130-
"\n\t--workspace to point on project directory"
131-
"\n\t--workspaceArchive to point on project .zip archive"
132-
"\n or neither to use current directory")
129+
if workspace_path not in ("none", None):
130+
path_type = utils.PathParser().parse_path(workspace_path)
131+
132+
if path_type == utils.PathParser.LOCAL_DIR:
133+
input_data["workspace"] = workspace_path
134+
else:
135+
if path_type == utils.PathParser.LOCAL_FILE:
136+
input_data["workspaceArchive"] = workspace_archive = workspace_path
137+
elif path_type in (utils.PathParser.GIT_URL, utils.PathParser.S3_URL):
138+
input_data["workspaceUrl"] = workspace_url = workspace_path
139+
140+
workspace_path = None
141+
input_data.pop("workspace", None)
142+
133143
return workspace_archive, workspace_path, workspace_url
134144

135145

tests/functional/test_experiments.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def test_should_send_proper_data_and_print_message_when_create_experiment_was_ru
8787
params=None,
8888
files=None,
8989
data=None)
90-
assert result.output == self.EXPECTED_STDOUT
90+
assert self.EXPECTED_STDOUT in result.output
9191
assert result.exit_code == 0
9292

9393
@mock.patch("gradient.client.requests.post")
@@ -104,7 +104,7 @@ def test_should_send_proper_data_and_print_message_when_create_experiment_was_ru
104104
params=None,
105105
files=None,
106106
data=None)
107-
assert result.output == self.EXPECTED_STDOUT
107+
assert self.EXPECTED_STDOUT in result.output
108108
assert result.exit_code == 0
109109
assert self.EXPECTED_HEADERS_WITH_CHANGED_API_KEY["X-API-Key"] == "some_key"
110110

@@ -122,7 +122,7 @@ def test_should_send_proper_data_and_print_message_when_create_wrong_project_id_
122122
params=None,
123123
files=None,
124124
data=None)
125-
assert result.output == self.EXPECTED_STDOUT_PROJECT_NOT_FOUND
125+
assert self.EXPECTED_STDOUT_PROJECT_NOT_FOUND in result.output
126126
assert result.exit_code == 0
127127

128128

@@ -145,7 +145,7 @@ class TestExperimentsCreateMultiNode(object):
145145
"--parameterServerCommand", "ls",
146146
"--parameterServerCount", 2,
147147
"--workerContainerUser", "usr",
148-
"--workspaceUrl", "some-workspace",
148+
"--workspace", "https://github.com/Paperspace/gradient-cli.git",
149149
]
150150
FULL_OPTIONS_COMMAND = [
151151
"experiments", "create", "multinode",
@@ -187,7 +187,7 @@ class TestExperimentsCreateMultiNode(object):
187187
"parameterServerCommand": u"ls",
188188
"parameterServerCount": 2,
189189
"workerContainerUser": u"usr",
190-
"workspaceUrl": "some-workspace",
190+
"workspaceUrl": "https://github.com/Paperspace/gradient-cli.git",
191191
}
192192
FULL_OPTIONS_REQUEST = {
193193
"name": u"multinode_mpi",
@@ -232,7 +232,7 @@ def test_should_send_proper_data_and_print_message_when_create_experiment_was_ru
232232
params=None,
233233
files=None,
234234
data=None)
235-
assert result.output == self.EXPECTED_STDOUT
235+
assert self.EXPECTED_STDOUT in result.output
236236
assert result.exit_code == 0
237237

238238
@mock.patch("gradient.client.requests.post")
@@ -249,7 +249,7 @@ def test_should_send_proper_data_and_print_message_when_create_experiment_was_ru
249249
params=None,
250250
files=None,
251251
data=None)
252-
assert result.output == self.EXPECTED_STDOUT
252+
assert self.EXPECTED_STDOUT in result.output
253253
assert result.exit_code == 0
254254

255255

@@ -304,7 +304,7 @@ class TestExperimentsCreateAndStartMultiNode(TestExperimentsCreateMultiNode):
304304
"--parameterServerCommand", "ls",
305305
"--parameterServerCount", 2,
306306
"--workerContainerUser", "usr",
307-
"--workspaceUrl", "some-workspace",
307+
"--workspace", "https://github.com/Paperspace/gradient-cli.git",
308308
"--no-logs",
309309
]
310310
FULL_OPTIONS_COMMAND = [

tests/functional/test_jobs.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ class TestJobsCreate(object):
357357
"--container", "testContainer",
358358
"--machineType", "testType",
359359
"--command", "testCommand",
360-
"--workspaceUrl", "some-workspace",
360+
"--workspace", "https://github.com/Paperspace/gradient-cli.git",
361361
]
362362
FULL_OPTIONS_COMMAND = [
363363
"jobs", "create",
@@ -383,8 +383,8 @@ class TestJobsCreate(object):
383383
"container": u"testContainer",
384384
"machineType": u"testType",
385385
"command": u"testCommand",
386-
"workspaceUrl": u"some-workspace",
387-
"workspaceFileName": u"some-workspace",
386+
"workspaceUrl": u"https://github.com/Paperspace/gradient-cli.git",
387+
"workspaceFileName": u"https://github.com/Paperspace/gradient-cli.git",
388388
}
389389
FULL_OPTIONS_REQUEST = {
390390
"name": u"exp1",
@@ -424,5 +424,5 @@ def test_should_send_proper_data_and_print_message_when_create_job_was_run_with_
424424
files=None,
425425
data=None)
426426

427-
assert result.output == self.EXPECTED_STDOUT
427+
assert self.EXPECTED_STDOUT in result.output
428428
assert result.exit_code == 0

0 commit comments

Comments
 (0)