Skip to content
This repository was archived by the owner on Aug 7, 2024. It is now read-only.
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
Binary file added testdata/corgi.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions testdata/post_upload_chunked_FINAL.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"image": {"image_type": "image/gif", "h": 270, "w": 500}, "media_id_string": "737956420046356480", "media_id": 737956420046356480, "expires_after_secs": 86400, "size": 2158019}
1 change: 1 addition & 0 deletions testdata/post_upload_chunked_INIT.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"media_id_string": "737956420046356480", "media_id": 737956420046356480, "expires_after_secs": 86400}
37 changes: 36 additions & 1 deletion tests/test_api_30.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ def setUp(self):
consumer_secret='test',
access_token_key='test',
access_token_secret='test',
sleep_on_rate_limit=False)
sleep_on_rate_limit=False,
chunk_size=500*1024)
self.base_url = 'https://api.twitter.com/1.1'
self._stderr = sys.stderr
sys.stderr = ErrNull()
Expand Down Expand Up @@ -1674,3 +1675,37 @@ def testMuteBlockParamsAndErrors(self):
resp = self.api.CreateMute(screen_name='jack', skip_status=True)
self.assertTrue(isinstance(resp, twitter.User))
self.assertFalse(resp.status)

@responses.activate
def testPostUploadMediaChunkedInit(self):
with open('testdata/post_upload_chunked_INIT.json') as f:
resp_data = f.read()
responses.add(responses.POST, DEFAULT_URL, body=resp_data, status=200)

with open('testdata/corgi.gif', 'rb') as fp:
resp = self.api._UploadMediaChunkedInit(fp)
self.assertEqual(len(responses.calls), 1)
self.assertEqual(resp[0], 737956420046356480)

@responses.activate
def testPostUploadMediaChunkedAppend(self):
media_fp, filename, _, _ = twitter.twitter_utils.parse_media_file(
'testdata/corgi.gif')
responses.add(responses.POST, DEFAULT_URL, body='', status=200)

resp = self.api._UploadMediaChunkedAppend(media_id=737956420046356480,
media_fp=media_fp,
filename=filename)
self.assertEqual(len(responses.calls), 7)
self.assertTrue(resp)

@responses.activate
def testPostUploadMediaChunkedFinalize(self):
with open('testdata/post_upload_chunked_FINAL.json') as f:
resp_data = f.read()

responses.add(responses.POST, DEFAULT_URL, body=resp_data, status=200)

resp = self.api._UploadMediaChunkedFinalize(media_id=737956420046356480)
self.assertEqual(len(responses.calls), 1)
self.assertTrue(resp)
108 changes: 91 additions & 17 deletions twitter/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -985,9 +985,8 @@ def PostUpdate(self,
else:
_, _, file_size, _ = parse_media_file(media)
if file_size > self.chunk_size:
media_ids = self.UploadMediaChunked(
media,
media_additional_owners)
media_ids.append(self.UploadMediaChunked(media,
media_additional_owners))
else:
media_ids.append(
self.UploadMediaSimple(media,
Expand Down Expand Up @@ -1079,11 +1078,11 @@ def PostMediaMetadata(self,

return resp

def UploadMediaChunked(self,
media,
additional_owners=None,
media_category=None):
""" Upload a media file to Twitter in multiple requests.
def _UploadMediaChunkedInit(self,
media,
additional_owners=None,
media_category=None):
"""Start a chunked upload to Twitter.

Args:
media:
Expand All @@ -1096,8 +1095,8 @@ def UploadMediaChunked(self,
API & video files.

Returns:
media_id:
ID of the uploaded media returned by the Twitter API or 0.
tuple: media_id (returned from Twitter), file-handler object (i.e., has .read()
method), filename media file.
"""
url = '%s/media/upload.json' % self.upload_url

Expand Down Expand Up @@ -1129,10 +1128,33 @@ def UploadMediaChunked(self,
except KeyError:
raise TwitterError({'message': 'Media could not be uploaded'})

boundary = bytes("--{0}".format(uuid4()), 'utf-8')
media_id_bytes = bytes(str(media_id).encode('utf-8'))
return (media_id, media_fp, filename)


def _UploadMediaChunkedAppend(self,
media_id,
media_fp,
filename):
"""Appends (i.e., actually uploads) media file to Twitter.

Args:
media_id (int):
ID of the media file received from Init method.
media_fp (file):
File-like object representing media file (must have .read() method)
filename (str):
Filename of the media file being uploaded.

Returns:
True if successful. Raises otherwise.
"""
url = '%s/media/upload.json' % self.upload_url

boundary = "--{0}".format(uuid4().hex).encode('utf-8')
media_id_bytes = str(media_id).encode('utf-8')
headers = {'Content-Type': 'multipart/form-data; boundary={0}'.format(
str(boundary[2:], 'utf-8'))}
boundary.decode('utf8')[2:]
)}

segment_id = 0
while True:
Expand All @@ -1154,9 +1176,9 @@ def UploadMediaChunked(self,
boundary,
b'Content-Disposition: form-data; name="segment_index"',
b'',
bytes(str(segment_id).encode('utf-8')),
str(segment_id).encode('utf-8'),
boundary,
bytes('Content-Disposition: form-data; name="media"; filename="{0}"'.format(filename), 'utf-8'),
'Content-Disposition: form-data; name="media"; filename="{0}"'.format(filename).encode('utf8'),
b'Content-Type: application/octet-stream',
b'',
data,
Expand All @@ -1182,7 +1204,20 @@ def UploadMediaChunked(self,
except:
pass

# Finalizing the upload:
return True

def _UploadMediaChunkedFinalize(self, media_id):
"""Finalize chunked upload to Twitter.

Args:
media_id (int):
ID of the media file for which to finalize the upload.

Returns:
json: JSON string of data from Twitter.
"""
url = '%s/media/upload.json' % self.upload_url

parameters = {
'command': 'FINALIZE',
'media_id': media_id
Expand All @@ -1191,10 +1226,49 @@ def UploadMediaChunked(self,
resp = self._RequestUrl(url, 'POST', data=parameters)
data = self._ParseAndCheckTwitter(resp.content.decode('utf-8'))

return data


def UploadMediaChunked(self,
media,
additional_owners=None,
media_category=None):
"""Upload a media file to Twitter in multiple requests.

Args:
media:
File-like object to upload.
additional_owners: additional Twitter users that are allowed to use
The uploaded media. Should be a list of integers. Maximum
number of additional owners is capped at 100 by Twitter.
media_category:
Category with which to identify media upload. Only use with Ads
API & video files.

Returns:
media_id:
ID of the uploaded media returned by the Twitter API. Raises if
unsuccesful.
"""

media_id, media_fp, filename = self._UploadMediaChunkedInit(media=media,
additional_owners=additional_owners,
media_category=media_category)

append = self._UploadMediaChunkedAppend(media_id=media_id,
media_fp=media_fp,
filename=filename)

if not append:
TwitterError('Media could not be uploaded.')

data = self._UploadMediaChunkedFinalize(media_id)

try:
return data['media_id']
except KeyError:
raise TwitterError({'message': 'Media could not be uploaded.'})
raise TwitterError('Media could not be uploaded.')


def PostMedia(self,
status,
Expand Down