Advertisement
stuppid_bot

Untitled

Jan 15th, 2016
278
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 25.21 KB | None | 0 0
  1. import hashlib
  2. import io
  3. import json
  4. import logging
  5. import os
  6. import platform
  7. import re
  8. import socket
  9. import time
  10. import urllib.parse
  11. import urllib.request
  12. import uuid
  13.  
  14. __author__ = "Sergei Snegirev (tz4678@gmail.com)"
  15. __copyright__ = "Copyright (C) 2013-2016 Sergei Snegirev"
  16. __license__ = "MIT"
  17. __version__ = "3.0"
  18. __url__ = "https://github.com/***/vkapi/"
  19.  
  20. __all__ = ('Client', 'ObjectDict', 'AccessToken', 'Permissions',
  21.            'PERMISSION_LIST', 'PERMISSION_STR', 'permissions2int',
  22.            'permissions2str', 'ClientError', 'ApiError', 'AuthError',
  23.            'UploadError')
  24.  
  25. HTTP_RE = re.compile('https?://', re.I)
  26. USER_AGENT_FORMAT = "Mozilla/5.0 (vkapi.Client/{ver} Python/{py_ver} +{url})"
  27. USER_AGENT = USER_AGENT_FORMAT.format(ver=__version__,
  28.                                       py_ver=platform.python_version(),
  29.                                       url=__url__)
  30.  
  31.  
  32. class Client:
  33.     api_version = 5.44
  34.     api_delay = 0.34
  35.     api_host = "api.vk.com"
  36.     api_path = "/method/"
  37.     auth_host = "oauth.vk.com"
  38.     auth_path = "token"
  39.     timeout = 15
  40.     retries = 3
  41.  
  42.     def __init__(self,
  43.                  access_token=None,
  44.                  client_id=None,
  45.                  client_secret=None,
  46.                  scope=None,
  47.                  username=None,
  48.                  password=None,
  49.                  api_version=None,
  50.                  api_delay=None,
  51.                  api_host=None,
  52.                  api_path=None,
  53.                  auth_host=None,
  54.                  auth_path=None,
  55.                  https=None,
  56.                  lang=None,
  57.                  opener=None,
  58.                  retries=None,
  59.                  timeout=None):
  60.         """Клиент для работы с API Вконтакте.
  61.  
  62.        + Нативная поддержка методов API
  63.        + Прямая авторизация
  64.        + Работа с подписью
  65.        + Загрузка файлов
  66.  
  67.        :param access_token: Токен полученный при авторизации
  68.        :type access_token: AccessToken instance
  69.  
  70.        :param client_id: Id приложения
  71.        :type client_id: int
  72.  
  73.        :param client_secret: Секретный ключ приложения
  74.        :type client_secret: str
  75.  
  76.        :param scope: Права доступа приложения
  77.        :type scope: str or int
  78.  
  79.        :param username: Имя пользователя
  80.        :type username: str
  81.  
  82.        :param password: Пароль
  83.        :type password: str
  84.  
  85.        :param api_version: Версия API
  86.        :type api_version: float
  87.  
  88.        :param api_delay: Задержка между вызовами методов API. Существуют
  89.            ограничения на количество обращений в секунду к методам API
  90.        :type api_delay: int or float
  91.  
  92.        :param api_host:
  93.        :type api_host: str
  94.  
  95.        :param api_path:
  96.        :type api_path: str
  97.  
  98.        :param auth_host:
  99.        :type auth_host: str
  100.  
  101.        :param auth_path:
  102.        :type auth_path: str
  103.  
  104.        :param https: Если равен 1, то методы API возвращают https ссылки на
  105.            фотографии и медиа файлы. Работает только если запросы
  106.            осуществляются через https.
  107.        :type https: int
  108.  
  109.        :param lang: Используемый язык (ru,ua,be,es,fi,de,it)
  110.        :type lang: str
  111.  
  112.        :param opener: Используется для отправки запросов
  113.        :type opener: urllib.request.OpenerDirector instance
  114.  
  115.        :param retries: Количество повторных запросов в случае ошибки таймаута.
  116.            Если значение равно -1, то запросы будут посылаться, пока не придет
  117.            ответ.
  118.        :type retries: int
  119.  
  120.        :param timeout: Таймаут
  121.        :type timeout: int or float
  122.        """
  123.         name = '{}.{}'.format(self.__module__, self.__class__.__name__)
  124.         self.logger = logging.getLogger(name)
  125.         self.access_token = access_token
  126.         self.client_id = client_id
  127.         self.client_secret = client_secret
  128.         self.scope = scope
  129.         self.username = username
  130.         self.password = password
  131.         self.api_version = api_version or self.api_version
  132.         self.api_delay = api_delay or self.api_delay
  133.         self.api_host = api_host or self.api_host
  134.         self.api_path = api_path or self.api_path
  135.         self.auth_host = auth_host or self.auth_host
  136.         self.auth_path = auth_path or self.auth_path
  137.         self.lang = lang
  138.         self.https = https
  139.         if not opener:
  140.             # Включаем поддержку cookies и используем системные прокси
  141.             opener = urllib.request.build_opener(
  142.                 urllib.request.HTTPCookieProcessor(),
  143.                 urllib.request.ProxyHandler())
  144.             opener.addheaders = [('User-Agent', USER_AGENT)]
  145.         self.opener = opener
  146.         self.timeout = timeout or self.timeout
  147.         self.retries = retries or self.retries
  148.         #
  149.         self.api = Api(self)
  150.         self.last_api_request = 0
  151.  
  152.     @property
  153.     def is_authorized(self):
  154.         return self.access_token is not None
  155.  
  156.     def api_request(self, method, params=None):
  157.         """Делает запрос к API.
  158.  
  159.        Список методов API: <https://vk.com/dev/methods>
  160.        Подробнее про запросы к API: <https://vk.com/dev/api_requests>
  161.  
  162.        Пример:
  163.            c = Client()
  164.            r = c.api_request('users.get', {'user_id': 1})
  165.            print("{u.first_name} {u.last_name}".format(u=r[0]))
  166.  
  167.        Использовать данный метод нет необходимости, так как к API можно
  168.        обращаться "нативно":
  169.            r = c.api.users.get(user_id=1)
  170.            # либо так
  171.            r = c.api.users.get({'user_id': 1})
  172.            # или так
  173.            r = c.api.users.get({'fields': 'photo'}, user_id=1)
  174.  
  175.        Это намного короче чем вызов api_request.
  176.  
  177.        :param method: Метод API
  178.        :type method: str
  179.  
  180.        :param params: Передаваемые параметры
  181.        :type params: dict
  182.  
  183.        :return: Возвращает результат запроса
  184.        """
  185.         params = dict(params or [])
  186.         if self.is_authorized:
  187.             params['access_token'] = self.access_token.value
  188.         # Позволяем в запросе выбрать версию API. Возможно, потребуется для
  189.         # обращения к каким-нибудь depractated методам
  190.         if 'v' not in params and self.api_version:
  191.             params['v'] = self.api_version
  192.         if 'lang' not in params and self.lang:
  193.             params['lang'] = self.lang
  194.         if 'https' not in params and self.https:
  195.             params['https'] = self.https
  196.         path = urllib.parse.urljoin(self.api_path, method)
  197.         # Кстати, через http ответы сервера приходят заметно быстрее
  198.         # <https://vk.com/dev/api_nohttps>
  199.         if hasattr(self.access_token, "secret") and self.access_token.secret:
  200.             scheme = 'http'
  201.             # Исправлен баг с неверной подписью. См. <path/to/lib/notes.txt>.
  202.             # Нам важно чтобы порядок элементов словаря сохранился.
  203.             # Добавим новый элемент
  204.             params['sig'] = ''
  205.             # Сформировали query string из словаря
  206.             query_string = urllib.parse.urlencode(params)
  207.             # А теперь вырежем параметр sig
  208.             query_string = re.sub('^sig=&|&sig=', '', query_string)
  209.             uri = '{}?{}{}'.format(path, query_string,
  210.                                    self.access_token.secret)
  211.             params["sig"] = hashlib.md5(uri.encode('ascii')).hexdigest()
  212.         else:
  213.             scheme = 'https'
  214.         # !!! params не должен изменяться после добавления sig
  215.         url = '{}://{}{}'.format(scheme, self.api_host, path)
  216.         payload = bytes(urllib.parse.urlencode(params), 'ascii')
  217.         delay = self.api_delay + self.last_api_request - time.time()
  218.         if delay > 0:
  219.             self.logger.debug("Wait %dms", delay * 1000)
  220.             time.sleep(delay)
  221.         self.logger.debug("Call method %r with parameters: %s", method, params)
  222.         try:
  223.             result = self.parse_json_response(self.http_request(url, payload))
  224.         except urllib.request.HTTPError as e:
  225.             self.logger.debug(e)
  226.             result = self.parse_json_response(e)
  227.         self.last_api_request = time.time()
  228.         if 'error' in result:
  229.             error = ApiError(result.error)
  230.         else:
  231.             error = None
  232.         response = result.get('response')
  233.         return self.process_api_response(response, error, method, params)
  234.  
  235.     def process_api_response(self, response, error, method, params):
  236.         """Метод для переопределения."""
  237.         if error:
  238.             if error.code is ApiError.CAPTCHA_NEEDED:
  239.                 captcha_key = self.get_captcha_key(error.captcha_img)
  240.                 params.update({'captcha_key': captcha_key,
  241.                                'captcha_sid': error.captcha_sid})
  242.                 return self.api_request(method, params)
  243.             raise error
  244.         return response
  245.  
  246.     def get_captcha_key(self, captcha_img):
  247.         raise NotImplementedError
  248.  
  249.     def test_access_token(self):
  250.         try:
  251.             return self.api.execute(code="return true;")
  252.         except ApiError:
  253.             return False
  254.  
  255.     def authorize(self,
  256.                   username=None,
  257.                   password=None,
  258.                   client_id=None,
  259.                   client_secret=None,
  260.                   scope=None,
  261.                   **kwargs):
  262.         """Метод для прямой авторизации <http://vk.com/dev/auth_direct>."""
  263.         if username:
  264.             self.username = username
  265.         if password:
  266.             self.password = password
  267.         if client_id:
  268.             self.client_id = client_id
  269.         if client_secret:
  270.             self.client_secret = client_secret
  271.         if scope:
  272.             self.scope = scope
  273.         # Обязательные параметры
  274.         if not self.username:
  275.             raise ClientError("username is not set")
  276.         if not self.password:
  277.             raise ClientError("password is not set")
  278.         if not self.client_id:
  279.             raise ClientError("client_id is not set")
  280.         if not self.client_secret:
  281.             raise ClientError("client_secret is not set")
  282.         params = dict(grant_type="password",
  283.                       client_id=self.client_id,
  284.                       client_secret=self.client_secret,
  285.                       username=self.username,
  286.                       password=self.password,
  287.                       v=self.api_version)
  288.         if self.scope:
  289.             params['scope'] = self.scope
  290.         if kwargs:
  291.             params.update(kwargs)
  292.         self.auth_request(params)
  293.  
  294.     def auth_request(self, params):
  295.         params = dict(params)
  296.         url = urllib.parse.urljoin("https://" + self.auth_host, self.auth_path)
  297.         url = "{}?{}".format(url, urllib.parse.urlencode(params))
  298.         try:
  299.             result = self.parse_json_response(self.http_request(url))
  300.         except urllib.request.HTTPError as e:
  301.             self.logger.debug(e)
  302.             result = self.parse_json_response(e)
  303.         if 'error' in result:
  304.             response = None
  305.             error = AuthError(result)
  306.         else:
  307.             response = result
  308.             error = None
  309.         self.process_auth_response(response, error, params)
  310.  
  311.     def process_auth_response(self, response, error, params):
  312.         """Метод для переопределения."""
  313.         if error:
  314.             # Если требуется ввод капчи
  315.             if error.type is AuthError.NEED_CAPTCHA:
  316.                 captcha_key = self.get_captcha_key(error.captcha_img)
  317.                 params.update({"captcha_key": captcha_key,
  318.                                "captcha_sid": error.captcha_sid})
  319.                 # Пробуем авторизоваться снова
  320.                 self.auth_request(params)
  321.             else:
  322.                 raise error
  323.         else:
  324.             self.create_auth_token(response)
  325.             self.logger.info("Successfully authorized")
  326.  
  327.     def create_auth_token(self, response):
  328.         self.access_token = AccessToken(value=response.access_token,
  329.                                         user_id=response.get('user_id'),
  330.                                         expires=response.get('expires_in'),
  331.                                         secret=response.get('secret'))
  332.         # При прямой авторизации всегда равен 0
  333.         if self.access_token.expires:
  334.             self.access_token.expires += time.time()
  335.  
  336.     def upload(self, server_url, files):
  337.         """
  338.        Загружает файлы на сервер.
  339.  
  340.        :param server_url: Адрес сервера
  341.        :type server_url: str
  342.  
  343.        :param files: Словарь где ключами выступают имена полей. Значения могут
  344.            быть строками (путь до файла либо ссылка на него), файловыми
  345.            объектами либо списками/кортежами,где первый элемент - имя файла,
  346.            а второй - его содержимое (бинарное либо текстовое)
  347.        :type files: dict
  348.  
  349.        :rtype: ObjectDict
  350.        """
  351.         boundary = uuid.uuid4().hex
  352.         buf = io.BytesIO()
  353.         for name, obj in files.items():
  354.             if isinstance(obj, str):
  355.                 # Загружаем по URL
  356.                 if HTTP_RE.match(obj):
  357.                     content = self.get_content(obj)
  358.                     # Всякие image.php?id=foo будут забракованы
  359.                     filename = os.path.basename(obj)
  360.                 # Читаем файл
  361.                 else:
  362.                     with open(obj, 'rb') as fp:
  363.                         content = fp.read()
  364.                     filename = os.path.basename(obj)
  365.             else:
  366.                 # {'photo': open("path/to/photo.jpg", 'rb')}
  367.                 if isinstance(obj, io.IOBase):
  368.                     filename = os.path.basename(obj.name)
  369.                     content = obj.read()
  370.                 # {'photo': ('photo.jpg', b'<binary data>')}
  371.                 elif isinstance(obj, (list, tuple)):
  372.                     # нам нужны только два первых элемента остальные будем
  373.                     # игнорировать
  374.                     filename, content, *_ = obj
  375.                 else:
  376.                     raise ValueError("Bad Value: {!r}".format(obj))
  377.  
  378.                 if not isinstance(content, (bytearray, bytes)):
  379.                     content = str(content).encode('utf-8')
  380.             # Поле Content-Type передавать необязательно,  на сервере Вконтакте
  381.             # проверяется расширение файла из поля filename и его содержиме,
  382.             # читаются первые n байтов, и ищутся сигнатуры
  383.             headers_str = ('--{}\r\n'
  384.                            'Content-Disposition: form-data; name="{}"; '
  385.                            'filename="{}"\r\n\r\n')
  386.             # У requests кстати баг, который заставил отказаться от его
  387.             # использования: он неправильно кодирует non-ascii поля в
  388.             # заголовках, поэтому файл "Мой файл.txt" в документах будет
  389.             # отображаться как "???....txt"
  390.             headers_str = headers_str.format(boundary, name, filename)
  391.             buf.write(bytes(headers_str, 'utf-8'))
  392.             buf.write(content)
  393.             buf.write(b'\r\n')
  394.         buf.write(bytes('--{}--\r\n'.format(boundary), 'utf-8'))
  395.         response = self.http_request(server_url, buf.getvalue(), {
  396.             'Content-Type': 'multipart/form-data; boundary=' + boundary})
  397.         result = self.parse_json_response(response)
  398.         if 'error' in result:
  399.             raise UploadError(result.error)
  400.         return result
  401.  
  402.     def parse_json(self, content):
  403.         return json.loads(content, object_hook=ObjectDict)
  404.  
  405.     def parse_json_response(self, response):
  406.         return self.parse_json(str(response.read(), "utf-8"))
  407.  
  408.     def get_content(self, url):
  409.         return self.http_request(url).read()
  410.  
  411.     def http_request(self, url, body=None, headers={}):
  412.         start_time = time.time()
  413.         self.logger.debug("Open %s", url)
  414.         request = urllib.request.Request(url, body, headers)
  415.         attempts = 0
  416.         while True:
  417.             try:
  418.                 response = self.opener.open(request, timeout=self.timeout)
  419.                 request_time = time.time() - start_time
  420.                 self.logger.debug("Total Request Time: %dms",
  421.                                   request_time * 1000)
  422.                 return response
  423.             except urllib.request.URLError as e:
  424.                 if isinstance(e.reason, socket.timeout):
  425.                     # Если значение равно -1, то запросы будут посылаться, пока
  426.                     # не придет ответ
  427.                     if self.retries != -1:
  428.                         attempts += 1
  429.                         if attempts > self.retries:
  430.                             raise
  431.                 else:
  432.                     raise
  433.  
  434.  
  435. class Api:
  436.     def __init__(self, client):
  437.         self._client = client
  438.  
  439.     def __getattr__(self, name):
  440.         if ApiMethod.NAME_RE.match(name):
  441.             return ApiMethod(self, name)
  442.         raise AttributeError(name)
  443.  
  444.  
  445. class ApiMethod:
  446.     NAME_RE = re.compile("[a-z]+([A-Z][a-z]+)*$")
  447.  
  448.     def __init__(self, client, method):
  449.         self._client = client
  450.         self._method = method
  451.  
  452.     def __getattr__(self, name):
  453.         if self.NAME_RE.match(name):
  454.             return ApiMethod(self._client, '.'.join([self._method, name]))
  455.         raise AttributeError(name)
  456.  
  457.     def __call__(self, *arg, **kw):
  458.         # Если имя именованного параметра совпадает с ключевым словом, то
  459.         # добавляем перед именем подчеркивание:
  460.         #
  461.         # client.storage.set(_global=1, key='foo', value='bar')
  462.         params = {k[1:] if len(k) > 1 and k.startswith('_')
  463.                   else k: v for k, v in kw.items()}
  464.         if len(arg):
  465.             # Первым аргументом является словарь
  466.             d = dict(arg[0])
  467.             # Если переданы именованные параметры, то обновляем словарь
  468.             d.update(params)
  469.             params = d
  470.         return self._client.api_request(self._method, params)
  471.  
  472.  
  473. class ObjectDict(dict):
  474.     def __getattr__(self, name):
  475.         try:
  476.             return self[name]
  477.         except KeyError:
  478.             raise AttributeError(name)
  479.  
  480.     def __setattr__(self, name, value):
  481.         self[name] = value
  482.  
  483.     def __repr__(self):
  484.         return "{}({})".format(self.__class__.__name__, super().__repr__())
  485.  
  486.  
  487. class AccessToken:
  488.     def __init__(self, value, expires=None, user_id=None, secret=None):
  489.         self.value = value
  490.         self.expires = expires
  491.         self.user_id = user_id
  492.         self.secret = secret
  493.  
  494.     def is_expired(self):
  495.         return self.expires and time.time() > self.expires
  496.  
  497.     def __repr__(self):
  498.         s = "{}(value={!r}, expires={!r}, user_id={!r}, secret={!r})"
  499.         return s.format(self.__class__.__name__, self.value, self.expires,
  500.                         self.user_id, self.secret)
  501.  
  502.  
  503. # Мы используем класс вместо обычных констант, потому как мы не можем получить
  504. # локальные переменные модуля (наши константы) так же просто:
  505. #
  506. # print(vars(Permissions))
  507. #
  508. # c = Client(
  509. #     ...
  510. #     scope=Permissions.FRIENDS | Permissions.VIDEO | Permissions.OFFLINE
  511. # )
  512. #
  513. class Permissions:
  514.     """Подробнее про права приложения можно прочитать по ссылке
  515.    <https://vk.com/dev/permissions>
  516.    """
  517.     NOTIFY = 1
  518.     FRIENDS = 2
  519.     PHOTOS = 4
  520.     AUDIO = 8
  521.     VIDEO = 16
  522.     DOCS = 131072
  523.     NOTES = 2048
  524.     PAGES = 128
  525.     STATUS = 1024
  526.     OFFERS = 32
  527.     QUESTIONS = 64
  528.     WALL = 8192
  529.     GROUPS = 262144
  530.     MESSAGES = 4096
  531.     EMAIL = 4194304
  532.     NOTIFICATIONS = 524288
  533.     STATS = 1048576
  534.     ADS = 32768
  535.     MARKET = 134217728
  536.     OFFLINE = 65536
  537.     ALL = 140492031
  538.  
  539.  
  540. PERMISSION_LIST = [v.lower() for v in dir(Permissions)
  541.                    if re.match('[A-Z]+$', v) and v != 'ALL']
  542. PERMISSIONS_STR = ",".join(PERMISSION_LIST)
  543.  
  544.  
  545. def permissions2int(s):
  546.     """Преобразует строку с правами доступа приложения в число
  547.  
  548.    >>> permissions2int('friends,video,offline')
  549.    65554
  550.    """
  551.     mask = 0
  552.     for v in s.split(','):
  553.         # Для сервера все равно передаем мы notify или NoTiFy
  554.         if v.lower() not in PERMISSION_LIST:
  555.             raise ValueError("Unknown permission: {}".format(v))
  556.         mask |= getattr(Permissions, v.upper())
  557.     return mask
  558.  
  559.  
  560. def permissions2str(mask):
  561.     """Преобразует числовое представление прав доступа приложения в строку
  562.  
  563.    >>> permissions2str(65554)
  564.    'friends,video,offline'
  565.    """
  566.     flags = mask
  567.     out = []
  568.     # Элементы в списке отсортированы по алфавиту
  569.     for v in PERMISSION_LIST:
  570.         flag = getattr(Permissions, v.upper())
  571.         if flag & flags == flag:
  572.             # Сбрасываем флаг. Правильно так делать flags &= ~flag, но это в
  573.             # случае, если неизвестно установлен флаг или нет
  574.             # 110 ^ 001 = 111
  575.             # 111 ^ 001 = 110
  576.             # 110 & ~001 = 110
  577.             # 111 & ~001 = 110
  578.             flags ^= flag
  579.             out.append(v)
  580.     if flags != 0:
  581.         raise ValueError("Bad mask: {}".format(mask))
  582.     return ','.join(out)
  583.  
  584.  
  585. class ClientError(Exception):
  586.     pass
  587.  
  588.  
  589. class ApiError(ClientError):
  590.     """Подробнее про ошибки API можно прочитать по ссылке
  591.    <https://vk.com/dev/errors>
  592.    """
  593.     UNKNOWN_ERROR_OCCURRED = 1
  594.     UNKNOWN_METHOD_PASSED = 3
  595.     INCORRECT_SIGNATURE = 4
  596.     USER_AUTHORIZATION_FAILED = 5
  597.     TOO_MANY_REQUESTS_PER_SECOND = 6
  598.     INVALID_REQUEST = 8
  599.     FLOOD_CONTROL = 9
  600.     INTERNAL_SERVER_ERROR = 10
  601.     CAPTCHA_NEEDED = 14
  602.     ACCESS_DENIED = 15
  603.     HTTP_AUTHORIZATION_FAILED = 16
  604.     VALIDATION_REQUIRED = 17
  605.     THIS_METHOD_WAS_DISABLED = 23
  606.     CONFIRMATION_REQUIRED = 24
  607.     INVALID_APPLICATION_API_ID = 101
  608.     INVALID_USER_ID = 113
  609.     INVALID_TIMESTAMP = 150
  610.     ACCESS_TO_ALBUM_DENIED = 200
  611.     ACCESS_TO_AUDIO_DENIED = 201
  612.     ACCESS_TO_GROUP_DENIED = 203
  613.     THIS_ALBUM_IS_FULL = 300
  614.     SOME_ADS_ERROR_OCCURED = 603
  615.  
  616.     def __init__(self, data):
  617.         self.msg = data.error_msg
  618.         self.code = data.error_code
  619.         self.captcha_img = data.get('captcha_img')
  620.         self.captcha_sid = data.get('captcha_sid')
  621.         self.redirect_uri = data.get('redirect_uri')
  622.         self.params = params = {
  623.             v['key']: v['value'] for v in data['request_params']}
  624.         self.method = params.pop('method')
  625.         self.oauth = params.pop('oauth')
  626.  
  627.     def __str__(self):
  628.         s = ("{} (Error Code: {}). An error occured while calling method {!r} "
  629.              "with parameters: {}")
  630.         return s.format(self.msg, self.code, self.method, self.params)
  631.  
  632.  
  633. class AuthError(ClientError):
  634.     NEED_CAPTCHA = 'need_captcha'
  635.     NEED_VALIDATON = 'need_validation'
  636.  
  637.     def __init__(self, data):
  638.         self.type = data.error
  639.         # Ошибка капчи не возвращает описание
  640.         self.description = data.get('error_description')
  641.         self.captcha_img = data.get('captcha_img')
  642.         self.captcha_sid = data.get('captcha_sid')
  643.         self.redirect_uri = data.get('redirect_uri')
  644.  
  645.     def __str__(self):
  646.         if not self.description:
  647.             return self.type
  648.         return "{}: {}".format(self.type, self.description)
  649.  
  650.  
  651. class UploadError(ClientError):
  652.     pass
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement