Skip to content

Commit b89c64f

Browse files
Add support for listJobArtifactsV2 endpoint to support pagination PS-15028 (#326)
1 parent bf65bc0 commit b89c64f

File tree

9 files changed

+89
-32
lines changed

9 files changed

+89
-32
lines changed

gradient/api_sdk/clients/job_client.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ def artifacts_get(self, job_id):
342342
data = repository.get(jobId=job_id)
343343
return data
344344

345-
def artifacts_list(self, job_id, files=None, size=False, links=True):
345+
def artifacts_list(self, job_id, files=None, size=False, links=True, start_after=None):
346346
"""
347347
Method to retrieve all artifacts files.
348348
@@ -354,20 +354,21 @@ def artifacts_list(self, job_id, files=None, size=False, links=True):
354354
job_id='your_job_id_here',
355355
files='your_files,here',
356356
size=False,
357-
links=True
357+
links=True,
358+
start_after='key',
358359
)
359360
360361
:param str job_id: to limit artifact from this job.
361362
:param str files: to limit result only to file names provided. You can use wildcard option ``*``.
362363
:param bool size: flag to show file size. Default value is set to False.
363364
:param bool links: flag to show file url. Default value is set to True.
365+
:params str start_after: key to list after
364366
365367
:returns: list of files with description if specified from job artifacts.
366-
:rtype: list[Artifact]
368+
:rtype: Pagination
367369
"""
368370
repository = self.build_repository(ListJobArtifacts)
369-
artifacts = repository.list(jobId=job_id, files=files, links=links, size=size)
370-
return artifacts
371+
return repository.list(jobId=job_id, files=files, links=links, size=size, start_after=start_after)
371372

372373
def get_metrics(self, job_id, start=None, end=None, interval="30s", built_in_metrics=None):
373374
"""Get job metrics

gradient/api_sdk/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .machine import Machine, MachineEvent, MachineUtilization
1010
from .model import Model, ModelFile
1111
from .notebook import Notebook, NotebookStart
12+
from .pagination import Pagination
1213
from .project import Project
1314
from .secret import Secret
1415
from .tag import Tag
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from datetime import datetime
2+
3+
import attr
4+
5+
6+
@attr.s
7+
class Pagination(object):
8+
"""
9+
Pagination class
10+
11+
:param list[Any] data:
12+
:param str start_after:
13+
"""
14+
data = attr.ib(type=list, default=None)
15+
start_after = attr.ib(type=str, default=None)

gradient/api_sdk/repositories/jobs.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,11 @@ def _parse_object(self, instance_dict, **kwargs):
126126

127127
class ListJobArtifacts(GetBaseJobApiUrlMixin, ListResources):
128128
def _parse_objects(self, data, **kwargs):
129-
serializer = serializers.ArtifactSchema()
130-
files = serializer.get_instance(data, many=True)
131-
return files
129+
serializer = serializers.utils.paginate_schema(serializers.ArtifactSchema)
130+
return serializer.get_instance(data)
132131

133132
def get_request_url(self, **kwargs):
134-
return "/jobs/artifactsList"
133+
return "/jobs/artifactsListV2"
135134

136135
def _get_request_params(self, kwargs):
137136
params = {
@@ -146,6 +145,9 @@ def _get_request_params(self, kwargs):
146145

147146
if kwargs.get("links"):
148147
params["links"] = kwargs.get("links")
148+
149+
if kwargs.get("start_after"):
150+
params["startAfter"] = kwargs.get("start_after")
149151

150152
return params
151153

gradient/api_sdk/s3_downloader.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,18 @@ class JobArtifactsDownloader(ResourceDownloader):
107107
CLIENT_CLASS = JobsClient
108108

109109
def _get_files_list(self, job_id):
110-
files = self.client.artifacts_list(job_id)
110+
start_after = None
111+
files = []
112+
while True:
113+
pagination_response = self.client.artifacts_list(job_id, start_after=start_after)
114+
115+
if pagination_response.data:
116+
files.extend(pagination_response.data)
117+
start_after = pagination_response.start_after
118+
119+
if start_after is None:
120+
break
121+
111122
files = tuple((f.file, f.url) for f in files)
112123
return files
113124

gradient/api_sdk/serializers/utils.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
import marshmallow
2+
3+
from .base import BaseSchema
4+
5+
from .. import models
16
from ..sdk_exceptions import GradientSdkError
27
from ..serializers import SingleNodeExperimentSchema, MultiNodeExperimentSchema, HyperparameterSchema, \
38
MpiMultiNodeExperimentSchema
49

10+
511
EXPERIMENT_ID_TO_EXPERIMENT_SERIALIZER_MAPPING = {
612
1: SingleNodeExperimentSchema,
713
2: MultiNodeExperimentSchema,
@@ -23,3 +29,12 @@ def get_serializer_for_experiment(experiment_dict):
2329
raise GradientSdkError("No experiment type with ID: {}".format(str(e)))
2430

2531
return serializer
32+
33+
34+
def paginate_schema(schema):
35+
class PaginationSchema(BaseSchema):
36+
MODEL = models.Pagination
37+
data = marshmallow.fields.Nested(schema, many=True, dump_only=True)
38+
start_after = marshmallow.fields.String(dump_to='startAfter', load_from='startAfter')
39+
40+
return PaginationSchema()

gradient/commands/jobs.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,17 @@ class ArtifactsListCommand(BaseJobCommand):
193193
def execute(self, **kwargs):
194194
with halo.Halo(text=self.WAITING_FOR_RESPONSE_MESSAGE, spinner="dots"):
195195
try:
196-
instances = self.client.artifacts_list(**kwargs)
196+
start_after = None
197+
instances = []
198+
while True:
199+
pagination_response = self.client.artifacts_list(start_after=start_after, **kwargs)
200+
201+
if pagination_response.data:
202+
instances.extend(pagination_response.data)
203+
start_after = pagination_response.start_after
204+
205+
if start_after is None:
206+
break
197207
except sdk_exceptions.GradientSdkError as e:
198208
raise exceptions.ReceivingDataFailedError(e)
199209

tests/example_responses.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5720,20 +5720,22 @@
57205720
"message": "success"
57215721
}
57225722

5723-
LIST_JOB_FILES_RESPONSE_JSON = [
5724-
{
5725-
"file": "hello.txt",
5726-
"url": "https://ps-projects.s3.amazonaws.com/some/path/artifacts/hello.txt?AWSAccessKeyId=some_aws_access_key_id&Expires=713274132&Signature=7CT5k6buEmZe5k5E7g6BXMs2xV4%3D&response-content-disposition=attachment%3Bfilename%3D%22hello.txt%22&x-amz-security-token=some_amz_security_token"
5727-
},
5728-
{
5729-
"file": "hello2.txt",
5730-
"url": "https://ps-projects.s3.amazonaws.com/some/path/artifacts/hello2.txt?AWSAccessKeyId=some_aws_access_key_id&Expires=713274132&Signature=L1lI47cNyiROzdYkf%2FF3Cm3165E%3D&response-content-disposition=attachment%3Bfilename%3D%22hello2.txt%22&x-amz-security-token=some_amz_security_token"
5731-
},
5732-
{
5733-
"file": "keton/elo.txt",
5734-
"url": "https://ps-projects.s3.amazonaws.com/some/path/artifacts/keton/elo.txt?AWSAccessKeyId=some_aws_access_key_id&Expires=713274132&Signature=tHriojGx03S%2FKkVGQGVI5CQRFTo%3D&response-content-disposition=attachment%3Bfilename%3D%22elo.txt%22&x-amz-security-token=some_amz_security_token"
5735-
}
5736-
]
5723+
LIST_JOB_FILES_RESPONSE_JSON = {
5724+
'data': [
5725+
{
5726+
"file": "hello.txt",
5727+
"url": "https://ps-projects.s3.amazonaws.com/some/path/artifacts/hello.txt?AWSAccessKeyId=some_aws_access_key_id&Expires=713274132&Signature=7CT5k6buEmZe5k5E7g6BXMs2xV4%3D&response-content-disposition=attachment%3Bfilename%3D%22hello.txt%22&x-amz-security-token=some_amz_security_token"
5728+
},
5729+
{
5730+
"file": "hello2.txt",
5731+
"url": "https://ps-projects.s3.amazonaws.com/some/path/artifacts/hello2.txt?AWSAccessKeyId=some_aws_access_key_id&Expires=713274132&Signature=L1lI47cNyiROzdYkf%2FF3Cm3165E%3D&response-content-disposition=attachment%3Bfilename%3D%22hello2.txt%22&x-amz-security-token=some_amz_security_token"
5732+
},
5733+
{
5734+
"file": "keton/elo.txt",
5735+
"url": "https://ps-projects.s3.amazonaws.com/some/path/artifacts/keton/elo.txt?AWSAccessKeyId=some_aws_access_key_id&Expires=713274132&Signature=tHriojGx03S%2FKkVGQGVI5CQRFTo%3D&response-content-disposition=attachment%3Bfilename%3D%22elo.txt%22&x-amz-security-token=some_amz_security_token"
5736+
}
5737+
],
5738+
}
57375739

57385740
GET_PRESIGNED_URL_FOR_S3_BUCKET_RESPONSE_JSON = {
57395741
"data": {

tests/functional/test_jobs.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -394,14 +394,14 @@ class TestListJobArtifacts(TestJobs):
394394

395395
@mock.patch("gradient.api_sdk.clients.http_client.requests.get")
396396
def test_should_send_valid_get_request_with_all_parameters_for_a_list_of_artifacts(self, get_patched):
397-
get_patched.return_value = MockResponse()
397+
get_patched.return_value = MockResponse(LIST_JOB_FILES_RESPONSE_JSON)
398398
job_id = "some_job_id"
399399
result = self.runner.invoke(cli.cli,
400400
["jobs", "artifacts", "list", "--id", job_id, "--apiKey", "some_key", "--size",
401401
"--links",
402402
"--files", "foo"])
403403

404-
get_patched.assert_called_with("{}/jobs/artifactsList".format(self.URL),
404+
get_patched.assert_called_with("{}/jobs/artifactsListV2".format(self.URL),
405405
headers=EXPECTED_HEADERS_WITH_CHANGED_API_KEY,
406406
json=None,
407407
params={"jobId": job_id,
@@ -412,11 +412,11 @@ def test_should_send_valid_get_request_with_all_parameters_for_a_list_of_artifac
412412

413413
@mock.patch("gradient.api_sdk.clients.http_client.requests.get")
414414
def test_should_read_options_from_yaml_file(self, get_patched, jobs_artifacts_list_config_path):
415-
get_patched.return_value = MockResponse()
415+
get_patched.return_value = MockResponse(LIST_JOB_FILES_RESPONSE_JSON)
416416
command = ["jobs", "artifacts", "list", "--optionsFile", jobs_artifacts_list_config_path]
417417
result = self.runner.invoke(cli.cli, command)
418418

419-
get_patched.assert_called_with("{}/jobs/artifactsList".format(self.URL),
419+
get_patched.assert_called_with("{}/jobs/artifactsListV2".format(self.URL),
420420
headers=EXPECTED_HEADERS_WITH_CHANGED_API_KEY,
421421
json=None,
422422
params={"files": "keton*.py",
@@ -434,12 +434,12 @@ def test_should_send_valid_get_request_with_valid_param_for_a_list_of_artifacts_
434434
get_patched,
435435
option,
436436
param):
437-
get_patched.return_value = MockResponse(status_code=200)
437+
get_patched.return_value = MockResponse(LIST_JOB_FILES_RESPONSE_JSON, status_code=200)
438438
job_id = "some_job_id"
439439
result = self.runner.invoke(cli.cli,
440440
["jobs", "artifacts", "list", "--id", job_id, "--apiKey", "some_key"] + [option])
441441

442-
get_patched.assert_called_with("{}/jobs/artifactsList".format(self.URL),
442+
get_patched.assert_called_with("{}/jobs/artifactsListV2".format(self.URL),
443443
headers=EXPECTED_HEADERS_WITH_CHANGED_API_KEY,
444444
json=None,
445445
params={"jobId": job_id,
@@ -635,7 +635,7 @@ def test_should_send_proper_data_and_tag_job(self, post_patched, get_patched, pu
635635

636636
class TestDownloadJobArtifacts(TestJobs):
637637
runner = CliRunner()
638-
LIST_FILES_URL = "https://api.paperspace.io/jobs/artifactsList"
638+
LIST_FILES_URL = "https://api.paperspace.io/jobs/artifactsListV2"
639639
DESTINATION_DIR_NAME = "dest"
640640
DESTINATION_DIR_PATH = os.path.join(tempfile.gettempdir(), "dest")
641641

0 commit comments

Comments
 (0)