Python API для Яндекс.Диск

Jan 30, 2014 11:55 · 1020 words · 5 minute read python

Дело было вечером, делать было нечего, вот и решил я написать обёртку для Яндекс Диска на python, дабы потом прикрутить скрипты бекапа для своих VPS. Готовое решение можно посмотреть на github или установить через pip: pip install YaDiskClient. Прежде чем начать, как полагается, посмотрел что же уже реализовано. Есть одна библиотека на PHP, которая удовлетворяет всем моим требованиям за исключением языка - хочется всё же работать на питоне. На pypi нашёл наработку от lexich. Вроде делает всё необходимое и даже больше, но какая-то она монстроидальная. Мне-то надо всего несколько методов. В общем, решил сам поковыряться с webdav и API Яндекс.Диска. К слову, авторизация в первом проекте реализована через OAuth, во втором - Basic. Я остановился на втором как наиболее простом. Класс для OAuth есть в первом коммите, может кому и пригодится :)

Yandex.Disk API

На странице API Yandex.Disk есть руководство как организовать авторизацию. Подключать к такой простой задаче как моя OAuth черезчур затратно, так что Basic. Для работы с сетью я использую библиотеку requests, в которой Basic-авторизация уже прозрачно реализована через передачу параметра auth=(login, password). По стандарту такой тип входа реализовывается посредством установки заголовка HTTP запроса “Authorization” в base_64("%s:%s” % (login, password)). Ура, теперь мы можем работать со своим облаком :)

Следующий шаг - выполнение примитивного запроса. Для примера я взял получение информации о свободном месте, отправив такой запрос:

PROPFIND / HTTP/1.1
Host: webdav.yandex.ru
Accept: */*
Depth: 0
Authorization: OAuth 0c4181a7c2cf4521964a72ff57a34a07

<D:propfind xmlns:D="DAV:">
  <D:prop>
    <D:quota-available-bytes/>
    <D:quota-used-bytes/>
  </D:prop>
</D:propfind>

К сожалению, requests поддерживает только REST-запросы, среди которых отсутствует тип PROPFIND, так что придётся делать через сессии:

req = requests.Request(type, url, headers=headers, auth=(self.login, self.password), data=data)
with requests.Session() as s:
return s.send(req.prepare())

где type='PROPFIND’, url=’/', headers={‘Accept’: ‘/', ‘Depth’: 0}, auth=(‘user@yandex.ru’, ‘password’), data=<та большая XML>. В ответ придёт XML, где в узлах “d:quota-available-bytes” и “d:quota-used-bytes” будет нужная информация. И тут вторая засада: Yandex использует namespace xmlns:d="DAV:". К сожалению, в lxml в методы выборки надо явно передавать пространства имён, участвующие в запросе. Т.е. tree.xpath("//d:prop”) - ничего не найдёт и даже ругнётся на префикс d, а вот tree.xpath("//d:prop”, {‘d’: “DAV:"}) - сработает на ура. На самом деле использование в данном случае lxml - из пушки по воробьям, достаточно и xml.dom.minidom.

Методы создания, копирования, удаления файлов тривиальны. Если кому-то будет интересно, то посмотреть можно здесь.

Тесты

Стало необходимым писать тесты для своих программ, так что побудем в тренде :) Для тестирования принято использовать библиотеку unittest. Документации по ней море, но мне бы хотелось остановиться на двух моментах. Первый - методы setUp и tearDown. Они выполняются перед и после каждого метода класса теста. В общем случае подключение к Диску может занимать заметное время, так что я вынес инициализацию в методы класса setUpClass и tearDownClass, которые будут вызыватся только один раз за прогон.

Второй момент - порядок выполнения тестов. По умолчанию он определяется по имени метода. Все методы с префиксом “test_” помещаются в список, сортируются и выполняются в таком порядке. Так что на это можно повлиять 2 способами: следить за именами тестовых методов или же создать набор тестов, куда помещать их в нужном порядке:

suite = unittest.TestSuite()
suite.addTest(TestYaDisk('test_mkdir'))
suite.addTest(TestYaDisk('download'))

К сожалению, для работы тестов нужны логин и пароль к облаку, так что придётся файл TestYaDisk немного подправить, указав LOGIN и PASSWORD.

Continuous Integration

Куда ж в наше время без CI?! Для Open Source проектов я нашёл 2 подходящих сервиса: TravisCI и CircleCI. Оба они достойны внимания, но я уловил некоторую разницу. TravisCI, как мне кажется, больше подходит для тестирования библиотек в разных окружениях. Его скрипт настройки для моего проекта выглядит както-то так:

language: python

os:
  - "linux"
  
python:
  - "2.7"
  - "3.3"
  - "3.4"
  - "3.5"
  - "3.6"
install:
  - pip install requests
  - pip install python-coveralls

script: 
  - coverage run --source='YaDiskClient' -m unittest discover -s tests -t tests
after_success:
  - coveralls

Сколько займёт добавление новой версии python? Менее 5 минут! Причём тесты запускаются параллельно, т.е. одновременно 5 работающих контейнеров. На CircleCI можно реализовать подобное поведение, но там в бесплатной версии дают максимум 4 контейнера. Он больше подходит для тестирования одного проекта с зависимыми сервисами (postgres, redis, elasticsearch…); собственно, я его и применил для своего GeoPuzzle, о чём и поделился в отдельной [статье].

Подключается TravisCI буквально в 2 клика:

  1. разрешить доступ на GitHub
  2. включить для конкретного проекта

TravisCI интерфейс

Вкладка “Settings” обычно скрывается под меню “More options”. Там можно задать переменные окружения, а также периодические выполнения задач, чтобы быть уверенным, что ничего не отвалилось.

Настройки TravisCI

Шилдики

Замечали, что у некоторых проектов в README.md показываются классные иконки со статусом сборки и покрытием? Тоже хочу такие же :)

шилдики YaDiskClient

Все они прописываются в README.md как ссылки на изображения (извините, у меня rst):

.. image:: https://travis-ci.org/TyVik/YaDiskClient.svg?branch=master
    :target: https://travis-ci.org/TyVik/YaDiskClient?branch=master
.. image:: https://coveralls.io/repos/github/TyVik/YaDiskClient/badge.svg?branch=master
    :target: https://coveralls.io/github/TyVik/YaDiskClient?branch=master
.. image:: https://img.shields.io/pypi/pyversions/YaDiskClient.svg
    :target: https://pypi.python.org/pypi/YaDiskClient/
.. image:: https://img.shields.io/pypi/v/YaDiskClient.svg
    :target: https://pypi.python.org/pypi/YaDiskClient/
.. image:: https://img.shields.io/pypi/status/YaDiskClient.svg
    :target: https://pypi.python.org/pypi/YaDiskClient/
.. image:: https://img.shields.io/pypi/l/YaDiskClient.svg
    :target: https://pypi.python.org/pypi/YaDiskClient/

Первый предоставляет сам TravisCI (он есть на странице проекта). Второй с покрытием тестами - сторонний сервис coveralls, который бесплатен для open source проектов. Остальные берутся из pypi.org с помощью сервиса shields.io. Необходимо лишь прописать специальные классификаторы в setup.py. Например, версия пакета - из version, статус определяется из ‘Development Status :: 5 - Production/Stable’; там же в classifiers и перечень версий python.

Создание Pypi пакета

И, наконец, самая важная часть - публикация пакета. Любая уважающая себя библиотека на Python должна быть доступна через pip. Для этого надо сначала зарегаться на pypi.python.org и создать файл .pypirc в корневой директории пользователя со следующим содержимым:

[distutils]
index-servers =
    pypi

[pypi]
username:<username>
password:<password>

Для создания пакета достаточно будет создать файлик setup.py, в котором будет вызываться процедура setuptools.setup. Пример можно посмотреть здесь. В нём описываются такие важные вещи как имя пакета, версия, зависимости, описание, ключевые слова, теги…

Для начала необходимо создать дистрибуцию: $ python setup.py sdist, которые автоматически помещаются в каталог dist, и которым присваивается имя проекта с номером версии. Можете попробовать сразу поставить его себе в виртуальное окружение: $ python setup.py install. После проверки всего одна команда $ python setup.py register sdist upload опубликует Ваш пакет.

Есть один момент, с которым я боролся очень долго - pip не понимает файлы README в формате .md, пришлось перегонять его в .rst командой

pandoc --from=markdown --to=rst --output=README.rst README.md

Эта утилита есть в репах Ubuntu, так что ставится через apt-get.

Итог

Ну что ж, мы создали полезную утилитку и сделали её доступной для всех :) Надеюсь, кому-нибудь пригодится :)