Skip to content
This repository was archived by the owner on Mar 18, 2019. It is now read-only.

Adding support to download be stream of data #170

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
09d80fb
Supporting big files
guydou Dec 10, 2017
ce0b6be
fix a bug
guydou Dec 10, 2017
4147bd9
allowing streaming on action
guydou Dec 10, 2017
552bce2
setting the stream part of the request
guydou Dec 10, 2017
a0efe9c
the stream should be part of the session
guydou Dec 10, 2017
5b05715
when mocking send we should get **kwargs since that is the signature …
guydou Dec 10, 2017
b597738
fixing test to support streaming
guydou Dec 10, 2017
7b4d4e1
fixing test to python3
guydou Dec 10, 2017
03a1317
dealing with the case the decoder returned a file
guydou Dec 10, 2017
fb1ca07
Supporting the case where there is error and DownloadCoded is enabled
guydou Dec 10, 2017
4f7d723
Merge branch 'error-with-download-coded' into dealing_with_errors_in_…
guydou Dec 11, 2017
3438917
when uploading file making sure it uses multipart/form
guydou Dec 12, 2017
fcc52c0
adding progress_bar to download
guydou Aug 15, 2018
7a93524
adding tqdm dep
guydou Aug 15, 2018
77c7680
Merge branch 'master' into adding_progress_bar_to_download
guydou Aug 15, 2018
a535c65
remore extra lines
guydou Aug 15, 2018
9edaf40
Update test_transport.py
guydou Aug 15, 2018
bf6191e
Update test_transport.py
guydou Aug 15, 2018
43a719b
Update requirements.txt
guydou Aug 15, 2018
3f50c5c
coding style
guydou Aug 15, 2018
2a45d03
Merge branch 'adding_progress_bar_to_download' of https://github.com/…
guydou Aug 15, 2018
0668871
code style
guydou Aug 15, 2018
8fb5573
Supporting the case where there is error and DownloadCoded is enabled
guydou Dec 10, 2017
6be714d
Merge branch 'master' into adding_progress_bar_to_download
guydou Aug 16, 2018
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
2 changes: 1 addition & 1 deletion coreapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from coreapi.document import Array, Document, Link, Object, Error, Field


__version__ = '2.3.3'
__version__ = '2.3.4'
__all__ = [
'Array', 'Document', 'Link', 'Object', 'Error', 'Field',
'Client',
Expand Down
4 changes: 2 additions & 2 deletions coreapi/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def reload(self, document, format=None, force_codec=False):
return self.get(document.url, format=format, force_codec=force_codec)

def action(self, document, keys, params=None, validate=True, overrides=None,
action=None, encoding=None, transform=None):
action=None, encoding=None, transform=None, stream=False):
if (action is not None) or (encoding is not None) or (transform is not None):
# Fallback for v1.x overrides.
# Will be removed at some point, most likely in a 2.1 release.
Expand Down Expand Up @@ -175,4 +175,4 @@ def action(self, document, keys, params=None, validate=True, overrides=None,

# Perform the action, and return a new document.
transport = determine_transport(self.transports, link.url)
return transport.transition(link, self.decoders, params=params, link_ancestors=link_ancestors)
return transport.transition(link, self.decoders, params=params, link_ancestors=link_ancestors, stream=stream)
9 changes: 7 additions & 2 deletions coreapi/codecs/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@ def dump(self, *args, **kwargs):
# Fallback for v1.x interface
return self.encode(*args, **kwargs)

def load(self, *args, **kwargs):
def support_streaming(self):
return False

def load(self, chunks, *args, **kwargs):
# Fallback for v1.x interface
return self.decode(*args, **kwargs)
if not(self.support_streaming()):
chunks = bytes().join(chunks) or bytes()
return self.decode(chunks, *args, **kwargs)

@property
def supports(self):
Expand Down
17 changes: 13 additions & 4 deletions coreapi/codecs/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import os
import posixpath
import tempfile
from tqdm import tqdm


def _unique_output_path(path):
Expand Down Expand Up @@ -102,26 +103,34 @@ class DownloadCodec(BaseCodec):
media_type = '*/*'
format = 'download'

def __init__(self, download_dir=None):
def __init__(self, download_dir=None, progress_bar=False):
"""
`download_dir` - The path to use for file downloads.
"""
self._delete_on_close = download_dir is None
self._download_dir = download_dir
self._progress_bar = progress_bar

def support_streaming(self):
return True

@property
def download_dir(self):
return self._download_dir

def decode(self, bytestring, **options):
def decode(self, chunks, **options):
base_url = options.get('base_url')
content_type = options.get('content_type')
content_disposition = options.get('content_disposition')

# Write the download to a temporary .download file.
fd, temp_path = tempfile.mkstemp(suffix='.download')
fd, temp_path = tempfile.mkstemp(suffix='.download', dir=self._download_dir)
file_handle = os.fdopen(fd, 'wb')
file_handle.write(bytestring)
content_length = options.get("content-length", None)
if content_length and self._progress_bar:
chunks = tqdm(chunks, total=content_length, unit="mb")
for chunk in chunks:
file_handle.write(chunk)
file_handle.close()

# Determine the output filename.
Expand Down
32 changes: 24 additions & 8 deletions coreapi/transports/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,14 @@ def _get_method(action):
return action.upper()


def _get_encoding(encoding):
def _get_encoding(encoding, params):
has_file = False
if params is not None:
for value in params.values():
if hasattr(value, 'read'):
has_file = True
if has_file:
return 'multipart/form-data'
if not encoding:
return 'application/json'
return encoding
Expand Down Expand Up @@ -226,7 +233,6 @@ def _build_http_request(session, url, method, headers=None, encoding=None, param
opts['data'] = params.data
upload_headers = _get_upload_headers(params.data)
opts['headers'].update(upload_headers)

request = requests.Request(method, url, **opts)
return session.prepare_request(request)

Expand Down Expand Up @@ -268,14 +274,18 @@ def _coerce_to_error(obj, default_title):
return Error(title=default_title, content={'messages': obj})
elif obj is None:
return Error(title=default_title)
elif hasattr(obj, "read"):
return Error(title=default_title, content={'messages': obj.read().decode("utf-8")})
return Error(title=default_title, content={'message': obj})


def _decode_result(response, decoders, force_codec=False):
"""
Given an HTTP response, return the decoded Core API document.
"""
if response.content:
chunk_size = 1024 * 1024
chunks = response.iter_content(chunk_size)
if chunks:
# Content returned in response. We should decode it.
if force_codec:
codec = decoders[0]
Expand All @@ -290,8 +300,9 @@ def _decode_result(response, decoders, force_codec=False):
options['content_type'] = response.headers['content-type']
if 'content-disposition' in response.headers:
options['content_disposition'] = response.headers['content-disposition']

result = codec.load(response.content, **options)
if 'content-length' in response.headers:
options["content-length"] = int(response.headers["content-length"]) / chunk_size
result = codec.load(chunks, **options)
else:
# No content returned in response.
result = None
Expand Down Expand Up @@ -366,19 +377,24 @@ def __init__(self, credentials=None, headers=None, auth=None, session=None, requ
def headers(self):
return self._headers

def transition(self, link, decoders, params=None, link_ancestors=None, force_codec=False):
def transition(self, link, decoders, params=None, link_ancestors=None, force_codec=False, stream=False):
session = self._session
method = _get_method(link.action)
encoding = _get_encoding(link.encoding)
encoding = _get_encoding(link.encoding, params)
params = _get_params(method, encoding, link.fields, params)
url = _get_url(link.url, params.path)
headers = _get_headers(url, decoders)
headers.update(self.headers)

request = _build_http_request(session, url, method, headers, encoding, params)

settings = session.merge_environment_settings(request.url, None, None, None, None)
settings["stream"] = stream
response = session.send(request, **settings)
result = _decode_result(response, decoders, force_codec)
result = None
if response.status_code != 204: # no content
result = _decode_result(response, decoders, force_codec)
response.close()

if isinstance(result, Document) and link_ancestors:
result = _handle_inplace_replacements(result, link, link_ancestors)
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ coreschema
itypes
requests
uritemplate
tqdm

# Testing requirements
coverage
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ def get_package_data(package):
'coreschema',
'requests',
'itypes',
'uritemplate'
'uritemplate',
'tqdm'
],
entry_points={
'coreapi.codecs': [
Expand Down
8 changes: 8 additions & 0 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ def __init__(self, content):
self.url = 'http://example.org'
self.status_code = 200

def iter_content(self, *args, **kwargs):
n = 2
list_of_chunks = list(self.content[i:i + n] for i in range(0, len(self.content), n))
return list_of_chunks

def close(self):
return


# Basic integration tests.

Expand Down
2 changes: 1 addition & 1 deletion tests/test_transitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
class MockTransport(HTTPTransport):
schemes = ['mock']

def transition(self, link, decoders, params=None, link_ancestors=None):
def transition(self, link, decoders, params=None, link_ancestors=None, stream=True):
if link.action == 'get':
document = Document(title='new', content={'new': 123})
elif link.action in ('put', 'post'):
Expand Down
7 changes: 7 additions & 0 deletions tests/test_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,16 @@ def __init__(self, content):
self.url = 'http://example.org'
self.status_code = 200

def iter_content(self, *args, **kwargs):
n = 2
list_of_chunks = list(self.content[i:i + n] for i in range(0, len(self.content), n))
return list_of_chunks

def close(self):
return
# Test transport errors.


def test_unknown_scheme():
with pytest.raises(NetworkError):
determine_transport(transports, 'ftp://example.org')
Expand Down