Skip to content

Add support for files endpoint #38

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,60 @@ __ https://docs.scale.com/reference#project-update-parameters
instruction="update: Please label all the stuff",
)

Files
________

Files are a way of uploading local files directly to Scale storage or importing files before creating tasks.

The ``file.attachment_url`` can be used in place of attachments in task payload.

Upload Files
^^^^^^^^^^^^^^

Upload a file. Check out `Scale's API documentation`__ for more information.

__ https://docs.scale.com/reference#file-upload-1

.. code-block:: python

with open(file_name, 'rb') as f:
my_file = client.upload_file(
file=f,
project_name = "test_project",
)

Import Files
^^^^^^^^^^^^^^

Import a file from a URL. Check out `Scale's API documentation`__ for more information.

__ https://docs.scale.com/reference#file-import-1

.. code-block:: python

my_file = client.import_file(
file_url="http://i.imgur.com/v4cBreD.jpg",
project_name = "test_project",
)


After the files are successfully uploaded to Scale's storage, you can access the URL as ``my_file.attachment_url``, which will have a prefix like ``scaledata://``.

The attribute can be passed to the task payloads, in the ``attachment`` parameter.

.. code-block:: python

task_payload = dict(
...
...
attachment_type = "image",
attachment = my_file.attachment_url,
...
...
)



Error handling
______________

Expand Down
58 changes: 55 additions & 3 deletions scaleapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import Dict, Generator, Generic, List, TypeVar, Union
from typing import IO, Dict, Generator, Generic, List, TypeVar, Union

from scaleapi.batches import Batch, BatchStatus
from scaleapi.exceptions import ScaleInvalidRequest
from scaleapi.files import File
from scaleapi.projects import Project

from ._version import __version__ # noqa: F401
Expand Down Expand Up @@ -317,7 +318,13 @@ def create_task(self, task_type: TaskType, **kwargs) -> Task:
taskdata = self.api.post_request(endpoint, body=kwargs)
return Task(taskdata, self)

def create_batch(self, project: str, batch_name: str, callback: str = "") -> Batch:
def create_batch(
self,
project: str,
batch_name: str,
callback: str = "",
instruction_batch: bool = False,
) -> Batch:
"""Create a new Batch within a project.
https://docs.scale.com/reference#batch-creation

Expand All @@ -329,12 +336,21 @@ def create_batch(self, project: str, batch_name: str, callback: str = "") -> Bat
callback (str, optional):
Email to notify, or URL to POST to
when a batch is complete.
instruction_batch (bool):
Only applicable for self serve projects.
Create an instruction batch by setting
the instruction_batch flag to true.

Returns:
Batch: Created batch object
"""
endpoint = "batches"
payload = dict(project=project, name=batch_name, callback=callback)
payload = dict(
project=project,
name=batch_name,
instruction_batch=instruction_batch,
callback=callback,
)
batchdata = self.api.post_request(endpoint, body=payload)
return Batch(batchdata, self)

Expand Down Expand Up @@ -596,3 +612,39 @@ def update_project(self, project_name: str, **kwargs) -> Project:
endpoint = f"projects/{Api.quote_string(project_name)}/setParams"
projectdata = self.api.post_request(endpoint, body=kwargs)
return Project(projectdata, self)

def upload_file(self, file: IO, **kwargs) -> File:
"""Upload file.
Refer to Files API Reference:
https://docs.scale.com/reference#file-upload-1

Args:
file (IO):
File buffer

Returns:
File
"""

endpoint = "files/upload"
files = {"file": file}
filedata = self.api.post_request(endpoint, files=files, data=kwargs)
return File(filedata, self)

def import_file(self, file_url: str, **kwargs) -> File:
"""Import file from a remote url.
Refer to Files API Reference:
https://docs.scale.com/reference#file-import-1

Args:
file_url (str):
File's url

Returns:
File
"""

endpoint = "files/import"
payload = dict(file_url=file_url, **kwargs)
filedata = self.api.post_request(endpoint, body=payload)
return File(filedata, self)
38 changes: 33 additions & 5 deletions scaleapi/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,20 @@ def __init__(self, api_key, user_agent_extension=None):
"Content-Type": "application/json",
"User-Agent": self._generate_useragent(user_agent_extension),
}
self._headers_multipart_form_data = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you specify a files parameter (a dictionary), then requests will send a multipart/form-data POST instead of a application/x-www-form-urlencoded POST.

ref: https://stackoverflow.com/questions/12385179/how-to-send-a-multipart-form-data-with-requests-in-python

"User-Agent": self._generate_useragent(user_agent_extension),
}

@staticmethod
def _http_request(
method, url, headers=None, auth=None, params=None, body=None
method,
url,
headers=None,
auth=None,
params=None,
body=None,
files=None,
data=None,
) -> Response:

https = requests.Session()
Expand All @@ -59,6 +69,8 @@ def _http_request(
auth=auth,
params=params,
json=body,
files=files,
data=data,
)

return res
Expand All @@ -77,13 +89,21 @@ def _raise_on_respose(res: Response):
raise exception(message, res.status_code)

def _api_request(
self, method, endpoint, headers=None, auth=None, params=None, body=None
self,
method,
endpoint,
headers=None,
auth=None,
params=None,
body=None,
files=None,
data=None,
):
"""Generic HTTP request method with error handling."""

url = f"{SCALE_ENDPOINT}/{endpoint}"

res = self._http_request(method, url, headers, auth, params, body)
res = self._http_request(method, url, headers, auth, params, body, files, data)

json = None
if res.status_code == 200:
Expand All @@ -99,10 +119,18 @@ def get_request(self, endpoint, params=None):
"GET", endpoint, headers=self._headers, auth=self._auth, params=params
)

def post_request(self, endpoint, body=None):
def post_request(self, endpoint, body=None, files=None, data=None):
"""Generic POST Request Wrapper"""
return self._api_request(
"POST", endpoint, headers=self._headers, auth=self._auth, body=body
"POST",
endpoint,
headers=self._headers
if files is None
else self._headers_multipart_form_data,
auth=self._auth,
body=body,
files=files,
data=data,
)

@staticmethod
Expand Down
21 changes: 21 additions & 0 deletions scaleapi/files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class File:
"""File class, containing File information."""

def __init__(self, json, client):
self._json = json
self.id = json["id"]
self.attachment_url = json["attachment_url"]
self._client = client

def __hash__(self):
return hash(self.id)

def __str__(self):
return f"File(id={self.id})"

def __repr__(self):
return f"File({self._json})"

def as_dict(self):
"""Returns all attributes as a dictionary"""
return self._json
15 changes: 15 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,3 +378,18 @@ def test_get_batches():
# Download all batches to check total count
all_batches = list(client.get_batches(project_name=TEST_PROJECT_NAME))
assert total_batches == len(all_batches)


def test_files_upload():
with open("tests/test_image.png", "rb") as f:
client.upload_file(
file=f,
project_name=TEST_PROJECT_NAME,
)


def test_files_import():
client.import_file(
file_url="https://static.scale.com/uploads/selfserve-sample-image.png",
project_name=TEST_PROJECT_NAME,
)
Binary file added tests/test_image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.