Показаны сообщения с ярлыком pyramid. Показать все сообщения
Показаны сообщения с ярлыком pyramid. Показать все сообщения

05 сентября 2014

REST API для Pyramid при помощи Cornice и SACRUD

Mozilla использует в своих проектах Pyramid и у них есть отличный модуль для создания REST API https://cornice.readthedocs.org/en/latest/

REST API обычно меняет, создает и удаляет записи, которые хранятся в БД. Что бы не писать много кода на SQLAlchemy я использую заготовленные функции из sacrud

Итак поехали, представим сервис REST API для платежных карт с моделью типа:

class Card(Base):
    __tablename__ = 'card'

    id = Column(Integer, primary_key=True)
    number = Column(BigInteger, nullable=False, unique=True)
    uid = Column(BYTEA, nullable=False, unique=True)
    balance = Column(Numeric(10, 2), nullable=False, default=0)
    preference = Column(GUID())

    def __json__(self):
        return {'id': self.id,
                'number': self.number,
                'uid': str(self.uid),
                'balance': str(self.balance)
                }

Создадим отдельную папку в pyramid проекте с названием rest,  где будут храниться функции api, и пропишем это в основном конфиге.

    # REST API
    config.include("cornice")
    config.include("myapp.rest", route_prefix="/api")

В pyramid'е есть такая штука как config.scan() она смотрит все файлы в проекте с расширением *.py (от текущей директории) и ищет вьюхи обернутые декоратором @view_config. Я предпочитаю включать в основной конфиг папочки через инклуд как в примере выше, а локально в каждой папке уже вызывать config.scan()

Структура папки rest
rest/
├── card.py
├── __init__.py
└── validators.py

Файл __init__.py

def includeme(config):
    config.scan()

В card.py сама реализация REST API

GET

import json

from cornice import Service

from myapp.models import DBSession
from myapp.models import Card
from myapp.rest.validators import _400, _404, valid_hex

from sacrud.action import CRUD
from sqlalchemy.exc import DataError
from sqlalchemy.orm.exc import NoResultFound

card_api = Service(name='card', path='/card/{UID}',
                   description="REST API for card")


@card_api.get(validators=valid_hex)
def get_card(request):
    """``GET``: возвращает данные о карте.

    .. code-block:: bash

        $ curl -D - http://0.0.0.0:6543/api/card/07a8e29d

        HTTP/1.1 200 OK
        Content-Length: 69
        Content-Type: application/json; charset=UTF-8
        Date: Sat, 02 Aug 2014 11:22:52 GMT
        Server: waitress

        {"uid": "07a8e29d", "balance": 507.05, "id": 3, "number": 8224548674}
    """
    key = request.validated['UID']
    card = DBSession.query(Card)
    if key:
        try:
            card = card.filter_by(uid=key).one()
        except NoResultFound:
            raise _404()
        except DataError:
            raise _400()
        return card.__json__()

Декоратор card_api превращает функцию get_card во вьюху, ожидающею HTTP метод GET, и config.scan() её автоматически подхватит. Внутри можно реализовывать все что угодно. Про метод validated ниже.

POST

@card_api.post(validators=valid_hex)
def set_card(request):
    """``POST``: передает параметры для редактирования карты.

    .. code-block:: bash

        $ curl -H 'Accept: application/json'\\
            -H 'Content-Type: application/json'\\
            http://0.0.0.0:6543/api/card/07a8e29d\\
            -d '{"number": "8224548674", "balance": "507.05"}'

        {"uid": "07a8e29d", "balance": 507.05, "id": 3, "number": 8224548674}

    Если карты нету, то создается новая

    .. code-block:: bash

        $ curl -H 'Accept: application/json'\\
            -H 'Content-Type: application/json'\\
            http://0.0.0.0:6543/api/card/550e8400\\
            -d '{"balance": "100.11", "preference": "956bfc40"}'

        {"uid": "550e8400", "balance": 100.11, "id": 7, "number": 91328937986}
    """
    key = request.validated['UID']
    data = {'request': json.loads(request.body)}

    card = DBSession.query(Card).filter_by(uid=key).first()
    if card:
        data['pk'] = {'id': card.id}
    else:
        data['request']['uid'] = key

    try:
        CRUD(DBSession, Card, **data).add()
    except Exception as e:
        raise _400(msg=str(e.message))
    card = DBSession.query(Card).filter_by(uid=key).one()
    return card.__json__()

Метод POST меняет параметры карты или создает новую если такой нету. Карта создается при помощи мега модуля sacrud. Подробнее о создании записей через sacrud здесь http://sacrud.readthedocs.org/en/latest/plain_usage.html#create-action

DELETE

@card_api.delete(validators=valid_hex)
def del_card(request):
    """``DELETE``: удаляет карту по UID

    .. code-block:: bash

        $ curl -H 'Accept: application/json'\\
            -H 'Content-Type: application/json'\\
            http://0.0.0.0:6543/api/card/550e8400 -X DELETE

        {"Goodbye": "550e8400"}
    """
    key = request.validated['UID']
    card = DBSession.query(Card).filter_by(uid=key).first()
    if not card:
        raise _404()
    try:
        CRUD(DBSession, Card, pk={'id': card.id}).delete()
    except DataError as e:
        raise _400(msg=str(e.message))
    return {'Goodbye': key}

Удаление происходит при вызове HTTP метода DELETE. sacrud опять же несколько упрощает эту операцию http://sacrud.readthedocs.org/en/latest/plain_usage.html#delete-action

В файле validators.py хранятся всякие исключения и сами проверки. Если в декораторе card_api указан валидатор, то валидные данные нужно будет выбирать не через request.matchdict а через request.validated

Пример файла validators.py

import json
import string

from webob import exc, Response


class _404(exc.HTTPError):
    def __init__(self, msg='Not Found'):
        body = {'status': 404, 'message': msg}
        Response.__init__(self, json.dumps(body))
        self.status = 404
        self.content_type = 'application/json'


class _400(exc.HTTPError):
    def __init__(self, msg='Bad Request'):
        body = {'status': 400, 'message': msg}
        Response.__init__(self, json.dumps(body))
        self.status = 400
        self.content_type = 'application/json'


def valid_hex(request):
    key = request.matchdict['UID']

    if not all(c in string.hexdigits for c in key):
        raise _400(msg="Not valid UID '%s'" % key)

    request.validated['UID'] = str(key)

Таким образом можно довольно просто создать API для вашего проекта на пирамиде. Cornice делает много за вас, создает вьюхи, пути, валидацию, может генерить автоматически  Sphinx документацию, а SACRUD упрощает работу с БД.

13 апреля 2014

Локальный Continuous Integration сервер

Идея непрерывной интеграции заключается в том, что при любом изменении проекта он пересобирается в условиях приближенных к реальной эксплуатации и каждый раз запускает тесты. Это позволяет моментально отловить баги и исправить их не отходя от кассы, пока ещё помнишь что понаписал.

Принцип работы у всех примерно один:
  • скачать код
  • создать окружение
  • установить (собрать) код
  • запустить и протестировать
  • отправить уведомление
Это можно сделать самостоятельно, например при помощи fabric, cron, chroot или docker или при помощи готовых CI серверов:
Облачные CI сервера:
Облачный CI отлично подойдет для OpenSource проектов, а остальное можно тестировать на локальном сервере.

Посмотрим как это работает на примере python + pyramid приложения


Начнём с Travis-ci
 https://travis-ci.org/
Следит за вашими репозитариями на github, выкачивает при каждом коммите, собирает окружение в индивидуальном контейнере каждый раз, имеет простой конфиг. Работает только с github, закрытые репы за деньги.

Авторизация через гитхаб аккаунт, дальше указываем что будем тестировать

Теперь положим в корень репы на гитхабе файл с настройками .travis.yml
В этом файле находятся инструкции, какое окружение нужно для вашего проекта, как его собирать, запускать тесты и куда слать уведомления.
language: python

notifications:
  email: "sacrud@uralbash.ru"
  email: "arkadiy@bk.ru"

python:
  - "2.7"
  - "2.6"

install:
  - pip install nose coverage coveralls
  - pip install pyramid pyramid_jinja2 pyramid_beaker
  - pip install -r requirements.txt

script:
  - nosetests --with-coverage --cover-package sacrud --cover-erase --with-doctest

after_success:
  coveralls
Здесь я думаю и так все понятно. coveralls нужен для сервиса https://coveralls.io/ (про него ниже).
После каждого коммита создается задание в трависе которое заканчивается примерно таким выводом https://travis-ci.org/ITCase/sacrud/jobs/22811094
Иногда СЕОшники всё портят как здесь https://travis-ci.org/ITCase/sacrud/jobs/22688250 т.к. не умеют запускать тесты, но благодаря CI эти проблемы сразу обнаруживаются.
Более сложный конфиг с установкой postgres  https://github.com/ITCase/pyramid_sacrud_example/blob/master/.travis.yml

drone.io
https://drone.io/  
Drone  похож на travis-ci но он дешевле, умеет bitbucket и исходный код https://github.com/drone/drone
Очень удобно OpenSource в облаке, приватные репы на локальном сервере и все это имеет одинаковый конфиг. Конфиг .drone.yml, формат очень похож на travis

image: python2.7
script:
  - pip install nose coverage pyramid pyramid_jinja2 pyramid_beaker
  - pip install -r requirements.txt
  - nosetests --cover-package=sacrud --cover-erase --with-coverage --with-doctest
notify:
  email:
    recipients:
      - sacrud@uralbash.ru

С облаком всё понятно, установим drone локально. В качестве платформы используется VM с Ubuntu server 12.04 с одним ядром и 2Гб ОЗУ, что вполне достаточно для небольшой команды программистов.
Т.к. drone собирает проекты в легковесных контенерах при помощи Docker вначале установим его http://docs.docker.io/en/latest/installation/ubuntulinux/#ubuntu-precise

Сам drone устанавливается очень просто через deb пакет http://drone.readthedocs.org/en/latest/install.html, теперь он у вас висит на 80 порту или на том который вы указали. Запускается и конфигурируется через upstart(sudo start drone, sudo stop drone). Можно проверить локально, если перейти в репозитарий проекта с файлом .drone.yml  и запустить drone build

Для того что бы github или bitbucket слал уведомление вашему drone серверу нужен статический IP. Пробросим порты к виртуалке на роутере :) и укажем IP в настройках

добавим репу

После добавления в гитхаб появится новый аппликайшин

Client ID и Client Secret нужно указать в настройках drone. Теперь комитим и чиним.

Для приватных реп drone автоматически  прописывает RSA ключ. Его можно посмотреть в настройках репы и скопировать вручную например или поменять.
Вывод похож на travis


Пару слов почему не другие системы. Во первых drone это и облако и локалхост, дальше bitbucket+github, контейнеры docker из коробки, иконка-статус сборки в Markdown, написан на Go как и Docker. Из недостатков пока мало свистелок и перделок, первое что бросается в глаза отсутствие кнопки REBUILD(пересобрать вручную). Но т.к. проект молодой то всё обещают запилить в следующей версии, судя по issue на github'е.
Jenkins страшный, сложный, всё пилить руками, докера нет, написан на Java.
Buildbot написан на python, хорошая архитектура master-slave, можно запилить slave в контейнеры, но написан на старой версии twisted и sqlalchemy аж 0.7 версии, код ужасен, инструкции из документации устаревшие, нужно додумывать, конфиг сложный, будущего у системы нет. 
StriderCD написан на nodejs, много чего есть из коробки, принцип плагинов через npm, docker пилить самому, глючный :( хотя выглядит неплохо.
Есть ещё альтернативы типа gitlab-ci и наверняка ещё что-то, но я их не смотрел.



Coveralls отличное дополнение, в котором можно визуально отследить что ещё не покрыто тестами. Из минусов, нет bitbucket, мне показался он дороговатым для приватных реп и ещё лежит отдавая 503 на момент написания этой статьи, но локальных альтернатив я не нашёл, к сожалению.

AnyKey
http://jmcvetta.github.io/blog/2013/08/30/continuous-integration-for-go-code/
http://lucapette.com/go/go-docker-and-a-ci-server/

И напоследок...
В этом обзоре показан простой пример как можно настроить непрерывную интеграцию проекта на python и pyramid. Но по аналогии можно поднять любой другой проект. Думаю вам эта статья поможет, хотя бы начать писать тесты :)