Статьи

Сериализация объектов сессии Python-Requests для удовольствия и прибыли

Первоначально авторство По Shrikant Шарат

Если вы еще не проверили @ kennethreitz отличного питона-запросы библиотеки пока я предлагаю вам пойти и сделать это немедленно. Давай, я буду ждать тебя.

У тебя была конфета? Это один из самых красивых фрагментов кода на Python, который я читал. И это отличная библиотека с очень гуманным API.

Недавно я использовал эту библиотеку для нескольких внутренних проектов моей компании, и в какой-то момент мне нужно было сериализовать и сохранить объекты Session для дальнейшего использования. Это было не так просто, как я думал, поэтому делюсь своим опытом здесь.

Прежде всего, давайте создадим простой http-сервер, с которым мы будем связываться при помощи python-запросов. Сервер должен иметь возможность обрабатывать сеансы на основе файлов cookie, а также иметь базовую аутентификацию, поскольку эти вещи обрабатываются объектами Session запросов Python на стороне клиента. Я не буду обсуждать здесь код для сервера, вы можете получить его из bitbucket .

Когда сервер запущен, теперь для клиента, давайте делать запросы!

import requests as req

URL_ROOT = 'http://localhost:5050'

def get_logged_in_session(name):
    session = req.session(auth=('user', 'pass'))

    login_response = session.post(URL_ROOT + '/login', data={'name': name})
    login_response.raise_for_status()

    return session

def get_whoami(session):
    response = session.get(URL_ROOT + '/whoami')
    response.raise_for_status()
    return response.text

Я определил две функции здесь. Get_logged_in_session создаст новый сеанс и войдет на http-сервер и вернет этот сеанс. Любые последующие запросы с использованием этого сеанса будут выполняться так, как если бы вы вошли в систему. Это то, что будет проверено с помощью функции get_whoami, которая будет просто возвращать ответ от / whoami.

Давайте проверим это. Убедитесь, что server.py запущен и находится в другом терминале,

$ python -i client.py
>>> s = get_logged_in_session('sharat')
>>> get_whoami(s)
u'You are sharat'
>>> get_whoami(req.session(auth=('user', 'pass')))
u'You are a guest'

Работает отлично. Если мы передаем ему зарегистрированный сеанс, он дает нам имя пользователя, а если мы передаем ему новый сеанс, он дает нам гостя.

Теперь давайте предположим, что у нас есть две функции: serialize_session и deserialize_session, которые делают именно то, что говорят их имена. Мы можем проверить их, запустив небольшой test.py, как

from client import get_logged_in_session, get_whoami
from serializer import deserialize_session, serialize_session

session = get_logged_in_session('sharat')
dsession = deserialize_session(serialize_session(session))

assert get_whoami(session) == get_whoami(dsession)
print 'Success'

и фиктивный serializer.py

def serialize_session(session):
    return session

def deserialize_session(session):
    return session

И с этим, конечно, тест не провалится

$ python test.py
Success

Сериализация

Теперь для реализации функций в serializer.py. Простым было бы использовать рассол. Давай попробуем

import pickle as pk

def serialize_session(session):
    return pk.dumps(session)

def deserialize_session(data):
    return pk.loads(data)

Если вы запустите test.py сейчас, Python будет кричать на вас.

$ python test.py
Traceback (most recent call last):
  File "test.py", line 10, in <module>
    dsession = deserialize_session(serialize_session(session))
[ ... ]
    raise TypeError, "can't pickle %s objects" % base.__name__
TypeError: can't pickle lock objects

Ну что ж, стоило попробовать, я полагаю.

Обновление : класс Session может быть создан для реализации протокола pickle, если вы хотите использовать pickle.

Следующим планом, который у меня был, было собрать атрибуты и данные из объекта Session, и этого было достаточно, чтобы воссоздать этот объект с помощью конструктора Session и сериализовать эти атрибуты как json. В конце концов, API-интерфейс Session очень прост в использовании, насколько сложно выбрать атрибуты из него? ?

Итак, я покопался в модуле sessions.py библиотеки python-запросов. А вот как выглядит подпись конструктора для объектов Session

def __init__(self,
    headers=None,
    cookies=None,
    auth=None,
    timeout=None,
    proxies=None,
    hooks=None,
    params=None,
    config=None,
    verify=True):
    # ...

Поэтому, если я выберу только эти значения, я смогу воссоздать объект сеанса. Милая.

import json
import requests as req

def serialize_session(session):
    attrs = ['headers', 'cookies', 'auth', 'timeout', 'proxies', 'hooks',
        'params', 'config', 'verify']

    session_data = {}

    for attr in attrs:
        session_data[attr] = getattr(session, attr)

    return json.dumps(session_data)

def deserialize_session(data):
    return req.session(**json.loads(data))

И давайте попробуем это

$ python test.py
Traceback (most recent call last):
  File "test.py", line 12, in <module>
    assert get_whoami(session) == get_whoami(dsession)
[ ... ]
[...]requests/models.py", line 447, in send
    r = self.auth(self)
TypeError: 'list' object is not callable

Хорошо, это сообщение об ошибке очень странно. Зачем кому-то называть объект списка?

Идите покопаться в модуле models.py . Посмотри это

[ ... ]
if isinstance(self.auth, tuple) and len(self.auth) == 2:
    # special-case basic HTTP auth
    self.auth = HTTPBasicAuth(*self.auth)

# Allow auth to make its changes.
r = self.auth(self)
[ ... ]

Там. Это не список, который называется. По крайней мере, не напрямую. Проблема здесь в том, что auth, который мы передаем для session (), не является кортежем. Duh! Хотя мне нравится, что auth ограничен быть кортежем, хотелось бы, чтобы было лучше сообщение об ошибке, когда auth является списком, а не кортежем. Я лично не хотел бы, чтобы это принимало список для аутентификации все же.

Итак, что пошло не так? JSON не различает кортеж и список. Это только списки. Таким образом, при сериализации и десериализации кортеж аутентификации превращается в список. Давайте вернемся назад

def deserialize_session(data):
    session_data = json.loads(data)

    if 'auth' in session_data:
        session_data['auth'] = tuple(session_data['auth'])

    return req.session(**session_data)

И

$ python test.py
Traceback (most recent call last):
  File "test.py", line 12, in <module>
    assert get_whoami(session) == get_whoami(dsession)
[ ... ]
  File "/usr/lib/python2.7/string.py", line 493, in translate
    return s.translate(table, deletions)
TypeError: translate() takes exactly one argument (2 given)

Подождите. Какая? Теперь у нас есть ошибка от stdlib? Это становится все лучше и лучше. Если это похоже на то, что может вас расстроить, принесите кофе ?

Если вы посмотрите на полную трассировку стека, второй файл снизу,

File "[...]site-packages/requests/packages/oreos/monkeys.py", line 470, in set
  if "" != translate(key, idmap, LegalChars):

Кажется, эта штука неправильно вызывает метод translate. После небольшой отладки и криков на мониторе я обнаружил проблему и на мгновение потерял контроль над реальностью.

str.translate принимает 2 аргумента, но unicode.translate принимает только 1. Я понятия не имею, почему это делается таким образом, но я уверен, что, черт возьми, это не понравилось. Код в oreos / monkeys.py предполагает, что ключ является str. Однако то, что дает вам json.loads, — это юникод. Итак, нам нужно преобразовать только части в десериализованном dict, который мы получаем из json.loads, который используется в oreos / monkeys.py, из unicode в str.

Прочитав немного кода вокруг библиотеки oreos, не потребовалось много времени, чтобы выяснить, что это были ключи в файле cookie. вот

def deserialize_session(data):
    session_data = json.loads(data)

    if 'auth' in session_data:
        session_data['auth'] = tuple(session_data['auth'])

    if 'cookies' in session_data:
        session_data['cookies'] = dict((key.encode(), val) for key, val in
                session_data['cookies'].items())

    return req.session(**session_data)

И так

$ python test.py
Success


!

Весь код находится в репозитории bitbucket .

Обновление: травление также может работать

Как отметил Дасльх в своем комментарии к Reddit, реализуя протокол Pickle в классе Session, мы можем заставить работать Pickle. Из документации нам нужны два метода, __getstate__ и __setstate__.

Добавление этих методов следующим образом в класс sessions.Session

def __getstate__(self):
    attrs = ['headers', 'cookies', 'auth', 'timeout', 'proxies', 'hooks',
        'params', 'config', 'verify']
    return dict((attr, getattr(self, attr)) for attr in attrs)

def __setstate__(self, state):
    for name, value in state.items():
        setattr(self, name, value)

    self.poolmanager = PoolManager(
        num_pools=self.config.get('pool_connections'),
        maxsize=self.config.get('pool_maxsize')
    )

с этой версией serializer.py, использующей pickle, мы получаем успех.

Создание нового пула-менеджера в __setstate__ — это фрагмент кода, скопированный из __init__ того же класса. Вероятно, следует обратиться к методу, чтобы избежать повторения кода.

Обновление 2 : создал проблему по этому поводу.

Обновление 3 : это было объединено, и объекты Session могут быть выбраны с версии 0.10.3. Смотрите историю запросов .

Источник: http://sharats.me/serializing-python-requests-session-objects-for-fun-and-profit.html