Skip to content

Commit

Permalink
Merge branch 'release/2.5.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
Bartek Kwiecien committed May 20, 2019
2 parents 13652fb + 474f63b commit 1c8f73f
Show file tree
Hide file tree
Showing 9 changed files with 239 additions and 34 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@
.cache/
temp_files/
.vscode/
test.zip
tests/data/test_download.jpg
.coverage
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Filestack-Python Changelog

### 2.5.0 (May 20th, 2019)
- Added support for [Filestack Workflows](https://www.filestack.com/products/workflows/)

### 2.4.0 (March 18th, 2019)
- Added default mimetype for multipart uploads
- Refactored multipart uploads
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.4.0
2.5.0
31 changes: 31 additions & 0 deletions examples/workflows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""
This script contains examples of using Filestack Workflows
in Python SDK. Workflows are compatible with uploading using
pathfile (by default with multipart=True) and external URL.
* It is essential to upload either from pathfile or url.
"""

from filestack import Client

Filestack_API_Key = '<YOUR_FILESTACK_API_KEY>'
file_path = '<PATH_TO_FILE>'
file_url = '<FILE_URL>'

client = Client(Filestack_API_Key)

# You should put your Workflows IDs in the parameters as a list.
store_params = {
'workflows': [
'<WORKFLOWS_ID_1>',
'<WORKFLOWS_ID_2>',
'<WORKFLOWS_ID_3>',
'<WORKFLOWS_ID_4>',
'<WORKFLOWS_ID_5>'
]
}

new_filelink = client.upload(
filepath=file_path,
# url=file_url,
params=store_params
)
90 changes: 59 additions & 31 deletions filestack/models/filestack_client.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import mimetypes
import os
import re
import requests

import filestack.models

from filestack.config import API_URL, CDN_URL, STORE_PATH
from filestack.config import API_URL, CDN_URL, STORE_PATH, HEADERS
from filestack.trafarets import STORE_LOCATION_SCHEMA, STORE_SCHEMA
from filestack.utils import utils
from filestack.utils import upload_utils
Expand Down Expand Up @@ -116,48 +117,75 @@ def upload(self, url=None, filepath=None, multipart=True, params=None, upload_pr
filelink = client.upload(filepath='/path/to/file', intelligent=False)
```
"""
if params:

if params: # Check the structure of parameters
STORE_SCHEMA.check(params)

if filepath and url:
if filepath and url: # Raise an error for using both filepath and external url
raise ValueError("Cannot upload file and external url at the same time")

if intelligent and filepath:
response = intelligent_ingestion.upload(
self.apikey, filepath, self.storage, params=params, security=self.security
)
elif multipart and filepath:
response = upload_utils.multipart_upload(
self.apikey, filepath, self.storage,
upload_processes=upload_processes, params=params, security=self.security
)
handle = response['handle']
return filestack.models.Filelink(handle, apikey=self.apikey, security=self.security)
else:
files, data = None, None
if url:
data = {'url': url}
if filepath:
if filepath: # Uploading from local drive
if intelligent:
response = intelligent_ingestion.upload(
self.apikey, filepath, self.storage, params=params, security=self.security
)

elif multipart:
response = upload_utils.multipart_upload(
self.apikey, filepath, self.storage,
upload_processes=upload_processes, params=params, security=self.security
)
handle = response['handle']
return filestack.models.Filelink(handle, apikey=self.apikey, security=self.security)

else: # Uploading with multipart=False
filename = os.path.basename(filepath)
mimetype = mimetypes.guess_type(filepath)[0]
files = {'fileUpload': (filename, open(filepath, 'rb'), mimetype)}

if params:
params['key'] = self.apikey
else:
params = {'key': self.apikey}
if params:
params['key'] = self.apikey
else:
params = {'key': self.apikey}

path = '{path}/{storage}'.format(path=STORE_PATH, storage=self.storage)

path = '{path}/{storage}'.format(path=STORE_PATH, storage=self.storage)
if self.security:
path = "{path}?policy={policy}&signature={signature}".format(
path=path, policy=self.security['policy'].decode('utf-8'),
signature=self.security['signature']
)

response = utils.make_call(
API_URL, 'post', path=path, params=params, files=files
)

else: # Uploading from an external URL
tasks = []
request_url_list = []

if utils.store_params_checker(params):
store_task = utils.store_params_maker(params)
tasks.append(store_task)

if self.security:
path = "{path}?policy={policy}&signature={signature}".format(
path=path, policy=self.security['policy'].decode('utf-8'),
signature=self.security['signature']
tasks.append(
'security=p:{policy},s:{signature}'.format(
policy=self.security['policy'].decode('utf-8'),
signature=self.security['signature']
)
)

response = utils.make_call(
API_URL, 'post', path=path, params=params, data=data, files=files
)
tasks = '/'.join(tasks)

if tasks:
request_url_list.extend((CDN_URL, self.apikey, tasks, url))
else:
request_url_list.extend((CDN_URL, self.apikey, url))

request_url = '/'.join(request_url_list)

response = requests.post(request_url, headers=HEADERS)

if response.ok:
response = response.json()
Expand All @@ -167,7 +195,7 @@ def upload(self, url=None, filepath=None, multipart=True, params=None, upload_pr
).group(1)
return filestack.models.Filelink(handle, apikey=self.apikey, security=self.security)
else:
raise Exception(response.text)
raise Exception('Invalid API response')

@property
def security(self):
Expand Down
5 changes: 4 additions & 1 deletion filestack/trafarets.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@
STORE_SCHEMA = t.Dict({
'filename': t.String(),
'mimetype': t.String(),
'location': t.String(),
'path': t.String(),
'container': t.String(),
'region': t.String(),
'access': t.String(),
'base64decode': t.Bool()
'base64decode': t.Bool(),
'workflows': t.List(t.String())
})

STORE_SCHEMA.make_optional('*')
5 changes: 5 additions & 0 deletions filestack/utils/upload_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ def multipart_upload(apikey, filepath, storage, upload_processes=None, params=No
request_data.update(start_response)
request_data['parts'] = ';'.join(uploaded_parts)

if params.get('workflows'):
workflows = ','.join('"{}"'.format(item) for item in params.get('workflows'))
workflows = '[{}]'.format(workflows)
request_data['workflows'] = workflows

complete_response = multipart_request(
'https://{}/multipart/complete'.format(location_url),
request_data,
Expand Down
24 changes: 24 additions & 0 deletions filestack/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,27 @@ def return_transform_task(transformation, params):
transformation_url = transformation

return transformation_url


def store_params_maker(params):
store_task = []

for key, value in params.items():
if key in ('filename', 'location', 'path', 'container', 'region', 'access', 'base64decode'):
store_task.append('{key}:{value}'.format(key=key, value=value))

if key is 'workflows':
workflows = ','.join('"{}"'.format(item) for item in value)
store_task.append('workflows:[{workflows}]'.format(workflows=workflows))

return 'store=' + ','.join(store_task)


def store_params_checker(params):
store_params_list = ['filename', 'location', 'path', 'container',
'region', 'access', 'base64decode', 'workflows']

if any(key in params for key in store_params_list):
return True
else:
return False
110 changes: 109 additions & 1 deletion tests/client_test.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
import filestack.models
import pytest

from mock import patch
from base64 import b64encode
from filestack import Client, Filelink, Transform
from httmock import urlmatch, HTTMock, response
from trafaret import DataError
from collections import defaultdict


APIKEY = 'APIKEY'
HANDLE = 'SOMEHANDLE'


class MockResponse():
ok = True
headers = {'ETag': 'some_tag'}

def __init__(self, json=None):
self.json_data = defaultdict(str) if json is None else json

def json(self):
return self.json_data


@pytest.fixture
def client():
return Client(APIKEY, security={'policy': b64encode(b'somepolicy'), 'signature': 'somesignature'})
Expand All @@ -26,7 +39,7 @@ def test_wrong_storage():


def test_store(client):
@urlmatch(netloc=r'www\.filestackapi\.com', path='/api/store', method='post', scheme='https')
@urlmatch(netloc=r'cdn.filestackcontent\.com', method='post', scheme='https')
def api_store(url, request):
return response(200, {'url': 'https://cdn.filestackcontent.com/{}'.format(HANDLE)})

Expand Down Expand Up @@ -81,3 +94,98 @@ def api_zip(url, request):
zip_response = client.zip('test.zip', 'tests/data/bird.jpg')

assert zip_response.status_code == 200


@pytest.mark.parametrize('store_params, expected_url_part', [
[
{
'filename': 'image.jpg'
},
'filename:image.jpg'
],
[
{
'location': 'S3'
},
'location:S3'
],
[
{
'path': 'some_path'
},
'path:some_path'
],
[
{
'container': 'container_id'
},
'container:container_id'
],
[
{
'region': 'us-east-1'
},
'region:us-east-1'
],
[
{
'access': 'public'
},
'access:public'
],
[
{
'base64decode': True
},
'base64decode:True'
],
[
{
'workflows': ['workflows_id_1']
},
'workflows:[%22workflows_id_1%22]'
]
])
def test_url_store_task(store_params, expected_url_part, client):
@urlmatch(netloc=r'cdn.filestackcontent\.com', method='post', scheme='https')
def api_store(url, request):
assert expected_url_part in request.url
return response(200, {'url': 'https://cdn.filestackcontent.com/{}'.format(HANDLE)})

with HTTMock(api_store):
filelink = client.upload(url="someurl", params=store_params, multipart=False)

assert isinstance(filelink, Filelink)
assert filelink.handle == HANDLE


@patch('requests.put')
@patch('requests.post')
def test_upload_multipart_workflows(post_mock, put_mock, client):

request_data = {'workflows': ['workflows_id']}
expected_request_data = {'workflows': '["workflows_id"]'}

put_mock.return_value = MockResponse()

post_mock.side_effect = [
MockResponse(),
MockResponse(),
MockResponse(json={'handle': 'new_handle'})
]

new_filelink = client.upload(
filepath='tests/data/bird.jpg',
params=request_data,
multipart=True
)

assert 'workflows' in post_mock.call_args[1]['data'].keys() and post_mock.call_args[1]['data']['workflows'] == expected_request_data['workflows']
assert new_filelink.handle == 'new_handle'

0 comments on commit 1c8f73f

Please sign in to comment.