Постим картинки в Twitter (допиливаем Tweepy)

Tweepy — лучшая обертка Twitter API для Python, на мой взгляд (остальные)
Базовые операции делаются очень легко и удобно:

import tweepy
# Чтобы получить консьюмер-ключи:
# идем https://dev.twitter.com/apps
# и жмем Create a new application
auth = tweepy.OAuthHandler(CONSUMER_KEY,CONSUMER_SECRET)
# Чтобы быстро авторизовать самого себя в приложении:
# жмем Create my access token на странице приложения
auth.set_access_token(ACCESS_TOKEN,ACCESS_TOKEN_SECRET)
api = tweepy.API(auth)
# получение логина и имени юзера
print api.me().screen_name, api.me().name
# отправка текстового твита
api.update_status('Hello, Twitter world!')
# фоловим другого юзера
api.get_user('muzhig').follow()

Но с загрузкой изображений дела обстоят немного иначе: в Tweepy она просто не реализована. Загрузка аватара и фона есть, а изображения в ленту — нет. Вооружившись напильником, исправим эту несправедливость.

Кому интересен результат, а не процесс, добро пожаловать в конец статьи. Начнем с файла api.py. В нем при помощи фабрики bind_api к классу API добавляются обвязки методов Twitter API.

update_status = bind_api(
    path = '/statuses/update.json',
    method = 'POST',
    payload_type = 'status',
    allowed_param = ['status', 'in_reply_to_status_id', 'lat', 'long', 'source', 'place_id'],
    require_auth = True
)

В api твиттера есть метод update_with_media для загрузки изображений, обвязку для которого мы должны создать схожим образом. У него есть несколько особенностей, которые несколько усложняют дело: во-вервых, судя по документации, для загрузки картинок нужно использовать хост upload.twitter.com вместо стандартного api.twitter.com, из-за чего придется допиливать bind_api. Во-вторых, изображение нужно загружать в теле POST-запроса, для формирования которого придется написать немного допольнительного кода. Давайте взглянем на метода загрузки картинки профиля:

""" account/update_profile_image """
def update_profile_image(self, filename):
    headers, post_data = API._pack_image(filename, 700)
    return bind_api(
        path = '/account/update_profile_image.json',
        method = 'POST',
        payload_type = 'user',
        require_auth = True
    )(self, post_data=post_data, headers=headers)

Видно, что заголовки и тело POST-запроса собираются при помощи метода _pack_image, после чего на лету собирается объект биндинга метода api, который тут же вызывается, получая в аргументы информацию содержимое для отправки.
Если посмотреть в _pack_image, то видно, что этот метод, кроме того, что собирает тело запроса, еще имеет логику для валидации типа, объема файла и размера изображения (видимо ограничения, связанные с аватарами и фоновыми изображениями) и выбрасывая соответствующие ошибки. Я решил разделить этот метод на два:

Первый, _pack_file, будет только паковать тело запроса из содержимого файла.

@staticmethod
def _pack_file(fp,filename="file.jpg",content_type='image/jpeg',field_name='image'):
    BOUNDARY = 'Tw3ePy'
    body = []
    body.append('--' + BOUNDARY)
    body.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (field_name,filename))
    body.append('Content-Type: %s' % content_type)
    body.append('')
    body.append(fp.read())
    body.append('--' + BOUNDARY + '--')
    body.append('')
    body = '\r\n'.join(body)

    # build headers
    headers = {
    'Content-Type': 'multipart/form-data; boundary=Tw3ePy',
    'Content-Length': len(body)
    }

    return headers, body

Вся же логика, связанная с изображениями профиля и фоном останется в _pack_image, после которой вызовется _pack_file:

""" Internal use only """

@staticmethod
def _pack_image(filename, max_size):
    """Pack image from file into multipart-formdata post body"""
    ...
    fp = open(filename, 'rb')
    result = API._pack_file(fp,filename,file_type)
    fp.close()
    return result

Теперь добавим в класс API определение upload_host:

class API(object):
    """Twitter API"""
    def __init__(self, ...
            host='api.twitter.com',
            search_host='search.twitter.com',
            ...
            upload_host='upload.twitter.com'):
        ...
        self.host = host
        self.search_host = search_host
        ...
        self.upload_host = upload_host

а в binder.py переключим хост на api.upload_host если в bind_api был передан аргумент with_media=True:

with_media = config.get(‘with_media’, False)
def __init__(self, api, args, kargs):
    ...
    if self.search_api:
        self.host = api.search_host
    elif self.with_media:
        self.host = api.upload_host
    else:
        self.host = api.host

Теперь можно делать нашу обвязку метода update_with_media:

""" /statuses/update_with_media """
def update_status_with_media(self, status, img):
    headers, post_data = API._pack_file(img,field_name="media[]")
    return bind_api(
        path = '/statuses/update_with_media.json',
        method = 'POST',
        payload_type = 'status',
        allowed_param = ['status', 'in_reply_to_status_id', 'lat', 'long', 'source', 'place_id'],
        require_auth = True,
        with_media=True
    )(self, post_data=post_data, headers=headers,  status=status)

PROFIT!

# загрузка изображения теперь не сложнее твита:
with open(filename,'rb') as f:
    api.update_status_with_media('Tweet text',f)

Исходный код (форк). Если нужно обновить уже установленный Tweepy — просто замените файлы api.py и binder.py файлами из моего форка.

Спасибо, что читаете!

Запись опубликована в рубрике Туториалы с метками , , , . Добавьте в закладки постоянную ссылку.

10 комментариев: Постим картинки в Twitter (допиливаем Tweepy)

  1. Хранитель говорит:

    А почему не отправляете в апстрим?
    Не принимают по идеологическим соображениям?

    • muzhig говорит:

      Я отправлял, но, по-моему, одновременно со мной еще кто-то занимался этой-же проблемой, и одобрили его пул-реквест, а не мой 🙂
      В любом случае цель достигнута- в новой версии Tweepy загрузка картинок работает (вроде).

      • Хранитель говорит:

        Хм, в документации и исходниках слёту не нашёл подходящего метода. Вижу только смену фона страницы и профиля

      • Хранитель говорит:

        Нда, полистал журнал проекта — там наверно с десяток желающих добавить эту функциональность, но ни один из вариантов так и не принят.
        А Вы в каком виде видели загрузку в официальной версии?

        • muzhig говорит:

          Хм, возможно и не приняли соседний пул-реквест. Я просто увидел, что там сделано один в один то же самое и запостили его чуть раньше меня, из чего я сделал вывод, что примут его, а не мой. 🙂

    • Хранитель говорит:

      Хм, а ведь таки отправлено, но не принято — уже больше года…

      • Alexander_35 говорит:

        Может дело в переходе с API v1 на API v1.1?

      • Alexander_35 говорит:

        Все заработало, спасибо за туториал. Только просто заменить файлы api.py и binder.py файлами из Вашего форка не покатит — нужно допиливать последнюю версию tweepy (что для API 1.1), с учетом изменения хоста для загрузки картинок (теперь стал стандартный api.twitter.com)

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *