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

22 сентября 2014

Документация python проекта на практике

Документация в python проектах пишется при помощи sphinx, он умеет используя расширение automodule читать докстринги и формировать документацию из кода.

Создать проект можно ответив на вопросы через sphinx-quickstart или использовать уже подготовленный шаблон который генерит дополнительно API пакета:
sphinx-apidoc -F -o docs sacrud

Creating file docs/sacrud_deform.rst.
Creating file docs/sacrud_deform.tests.rst.
Creating file docs/conf.py.
Creating file docs/index.rst.
Creating file docs/Makefile.
Creating file docs/make.bat.

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


Для сборки доков  нужно в папке docs выполнить make html. Ниже структура получившихся файлов:
.
├── _build
│   ├── doctrees
│   │   ├── environment.pickle
│   │   ├── index.doctree
│   │   ├── readme.doctree
│   │   ├── sacrud_deform.doctree
│   │   └── sacrud_deform.tests.doctree
│   └── html
│       ├── genindex.html
│       ├── index.html
│       ├── _modules
│       │   ├── index.html
│       │   ├── sacrud_deform
│       │   │   ├── tests
│       │   │   │   └── test_form.html
│       │   │   └── widgets.html
│       │   └── sacrud_deform.html
│       ├── objects.inv
│       ├── py-modindex.html
│       ├── readme.html
│       ├── sacrud_deform.html
│       ├── sacrud_deform.tests.html
│       ├── search.html
│       ├── searchindex.js
│       ├── _sources
│       │   ├── index.txt
│       │   ├── readme.txt
│       │   ├── sacrud_deform.tests.txt
│       │   └── sacrud_deform.txt
│       └── _static
│           ├── ajax-loader.gif
│           ├── basic.css
│           ├── comment-bright.png
│           ├── comment-close.png
│           ├── comment.png
│           ├── default.css
│           ├── doctools.js
│           ├── down.png
│           ├── down-pressed.png
│           ├── file.png
│           ├── jquery.js
│           ├── minus.png
│           ├── plus.png
│           ├── pygments.css
│           ├── searchtools.js
│           ├── sidebar.js
│           ├── underscore.js
│           ├── up.png
│           ├── up-pressed.png
│           └── websupport.js
├── conf.py
├── index.rst
├── make.bat
├── Makefile
├── readme.rst
├── sacrud_deform.rst
├── _static
└── _templates

В директории build/html находится наша готовая документация в формате html. Добавим описание проекта на основную страницу. Для этого создаем readme.rst в директории docs и включаем его в index.rst, а в дальнейшем этот же readme.rst будет использоваться в README.rst в корне проекта для github и PyPi.

readme.rst
|Build Status| |Coverage Status| |Stories in Progress| |PyPI|

.. |Build Status| image:: https://travis-ci.org/ITCase/sacrud_deform.svg?branch=master
   :target: https://travis-ci.org/ITCase/sacrud_deform
.. |Coverage Status| image:: https://coveralls.io/repos/ITCase/sacrud_deform/badge.png?branch=master
   :target: https://coveralls.io/r/ITCase/sacrud_deform?branch=master
.. |Stories in Progress| image:: https://badge.waffle.io/ITCase/sacrud_deform.png?label=in%20progress&title=In%20Progress
   :target: http://waffle.io/ITCase/sacrud_defrom
.. |PyPI| image:: http://img.shields.io/pypi/dm/sacrud_deform.svg
   :target: https://pypi.python.org/pypi/sacrud_deform/

sacrud_deform
==============

Form generotor for SQLAlchemy models.

Install
=======

develop version from source

.. code-block:: bash

  pip install git+git://github.com/ITCase/sacrud_deform@develop

from pypi

.. code-block:: bash

  pip install sacrud_deform

Use
===

.. code-block:: python

        data = form_generator(dbsession=DBSession,
                                   obj=obj_of_model, table=MyModel,
                                   columns=columns_of_model)
        form, js_list = data.render()

Здесь вроде все понятно, но есть нюансы, если например в директиве .. code-block не указать язык или не дописывать "=" в заголовках то документация будет нормально генериться, но PyPi её не поймет и выведет что-то типа этого:

Нужно быть внимательным и проверять как PyPi подхватил README.rst. Дальше в наш index.rst добавим readme.rst что бы он был по презентабельнее.

index.rst

Welcome to sacrud_deform's documentation!
=========================================

.. include:: readme.rst

Contents:

.. toctree::
   :maxdepth: 4

   sacrud_deform


Indices and tables
==================

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

Теперь главная страница документации выглядит так:

В принципе уже хорошо, добавим это описание ещё для github и PyPi. Они по умолчанию берут файл README.rst из корня проекта и ничего про папку docs не знают. Можно было бы заинклудить readme.rst в корневой README.rst(..include:: docs/readme.rst), но github и PyPi инклуды не понимают, поэтому придется писать свой велосипед для копирования. Либо тупо делать это вручную. Я написал на коленке простой скрипт который это делает и заменяет директивы include содержанием их файлов.

make_README.py
import fileinput
import os
from shutil import copyfile

src = "readme.rst"
src_path = os.path.dirname(os.path.realpath(src))
dst = "../README.rst"
copyfile(src, dst)


def read_file(path):
    with open(path, 'r') as f:
        return f.read()

for line in fileinput.input(dst, inplace=1):
    splitted = line.rstrip().split('.. include:: ')
    if len(splitted) == 2:
        line = read_file(os.path.join(src_path, splitted[1]))
        print line
    else:
        print line.rstrip()

Теперь если запустить из папки docs команду python make_README.py, то в корне проекта появится или перезапишется файл README.rst

Что бы это делалось автоматически при каждой сборке документации добавим в Makefile:
...

readme:
 python make_README.py

readme_html: html readme

При вызове "make readme_html" у нас будет собираться документация html и копипаститься readme.

Конфиг для sphinx лежит в папке документации с названием conf.py. В нем можно настраивать документацию как угодно, к примеру оформим тему как у проекта Pyramid.

Добавим в conf.py
import sys
import os

# Add and use Pylons theme
if 'sphinx-build' in ' '.join(sys.argv):  # protect against dumb importers
    from subprocess import call, Popen, PIPE

    p = Popen('which git', shell=True, stdout=PIPE)
    git = p.stdout.read().strip()
    cwd = os.getcwd()
    _themes = os.path.join(cwd, '_themes')

    if not os.path.isdir(_themes):
        call([git, 'clone', 'git://github.com/Pylons/pylons_sphinx_theme.git',
              '_themes'])
    else:
        os.chdir(_themes)
        call([git, 'checkout', 'master'])
        call([git, 'pull'])
        os.chdir(cwd)

    sys.path.append(os.path.abspath('_themes'))

    parent = os.path.dirname(os.path.dirname(__file__))
    sys.path.append(os.path.abspath(parent))
    wd = os.getcwd()
    os.chdir(parent)
    os.chdir(wd)

    for item in os.listdir(parent):
        if item.endswith('.egg'):
            sys.path.append(os.path.join(parent, item))

Этот код я скопировал из проекта Pyramid, он просто выкачивает их тему с github в директорию _themes. Далее укажем в конфиге название темы и где их искать.
# -- Options for HTML output ----------------------------------------------

# The theme to use for HTML and HTML Help pages.  See the documentation for

# a list of builtin themes.

html_theme = 'pyramid'

# Add any paths that contain custom themes here, relative to this directory.

html_theme_path = ['_themes']

После сборки проект будет выглядеть так:

Вообщем конфиг умеет много чего, есть еще много разных расширений, можно писать свои, либо переопределять/добавлять директивы итд. Вот например как вставлять ссылки на сторонние проекты:
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions += [
    'sphinx.ext.intersphinx'
]

intersphinx_mapping = {
    'sacrud': ('http://sacrud.readthedocs.org/en/latest/', None),
}

Теперь можно писать так:
Use :py:class:`sacrud.common.TableProperty` decorator.

И наверно последний этап это добавление документации на readthedocs. Сложностей там особо нету, регаешься, добавляешь гитхаб профиль, синхронизируешь репы и выбираешь нужные. Хук на коммиты в таком случае вешается автоматически, единственное что нужно отметить что если в документации используется automodule нужно ставить галку virtualenv(в настройках readthedocs) иначе он просто будет пустым.
Готовый пример можно посмотреть здесь http://sacrud-deform.readthedocs.org/en/develop/

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 упрощает работу с БД.

19 августа 2014

Chameleon, deform и маленькая хитрость

Deform - это такая штука которая генерит формы, а шаблоны для виджетов в нем написаны в формате Chameleon шаблонизатора.

Простой пример формы(http://deformdemo.xo7.de/nonrequiredfields/):
        class Schema(colander.Schema):
            required = colander.SchemaNode(
                colander.String(),
                description='Required Field'
                )
            notrequired = colander.SchemaNode(
                colander.String(),
                missing=unicode(''),
                description='Unrequired Field')
        schema = Schema()
        form = deform.Form(schema, buttons=('submit',))
        return self.render_form(form)



К полям можно добавлять описание, но если вставить html то он заэскапируется(https://github.com/Pylons/deform/blob/bb4fc86913884deafa9350de86d87fb5232263fa/deform/templates/form.pt#L42). Можно воспользоваться хитрой возможностью Chamelon'а что бы вывести html.

class HTMLText(object):
    def __init__(self, text):
        self.text = text

    def __html__(self):
        return unicode(self.text)

notrequired = colander.SchemaNode(
                colander.String(),
                missing=unicode(''),
                description=HTMLText('Hello <hr color="red" /> World!!! <hr />'))

Chameleon по умолчанию ищет метод __html__ у объектов и если он есть то выводит его результат в чистом виде. Вот.

12 мая 2014

MPTT для SQLAlchemy

Запилил я тут для своих нужд небольшое приложение которое добавляет в модель поля и функционал необходимый для Nested sets. По аналогии с django-mptt. Грубо говоря в модель добавляются поля left и right которые при помощи системы эвентов самостоятельно пересчитываются при изменении дерева. Ниже пример обхода дерева:
 Простой пример:


from sqlalchemy import Column, Integer, Boolean
from sqlalchemy.ext.declarative import declarative_base

from sqlalchemy_mptt.mixins import BaseNestedSets

Base = declarative_base()


class Tree(Base, BaseNestedSets):
    __tablename__ = "tree"

    id = Column(Integer, primary_key=True)
    visible = Column(Boolean)  # Наше кастомное поле

    def __repr__(self):
        return "" % self.id

Tree.register_tree()  # Регистрирует event'ы

Теперь попробуем добавить ноду:

node = Tree(parent_id=6)
session.add(node)
получим
        level           Nested sets example
        1                    1(1)22
                _______________|___________________
               |               |                   |
        2    2(2)5           6(4)11             12(7)21
               |               ^                   ^
        3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                              |          |
        4                                  14(9)15   18(11)19

        level     Insert node with parent_id == 6
        1                    1(1)24
                _______________|_________________
               |               |                 |
        2    2(2)5           6(4)13           14(7)23
               |           ____|____          ___|____
               |          |         |        |        |
        3    3(3)4      7(5)8    9(6)12  15(8)18   19(10)22
                                   |        |         |
        4                      10(23)11  16(9)17  20(11)21

Удаление:
node = session.query(Tree).filter(Tree.id == 4).one()
session.delete(node)
получим
        level           Nested sets example
        1                    1(1)22
                _______________|___________________
               |               |                   |
        2    2(2)5           6(4)11             12(7)21
               |               ^                   ^
        3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                              |          |
        4                                  14(9)15   18(11)19

        level         Delete node == 4
        1                    1(1)16
                _______________|_____
               |                     |
        2    2(2)5                 6(7)15
               |                     ^
        3    3(3)4            7(8)10   11(10)14
                                |          |
        4                     8(9)9    12(11)13

Обновление:
node = session.query(Tree).filter(Tree.id == 8).one()
node.parent_id = 5
session.add(node)
получим
        level           Nested sets example
            1                    1(1)22
                    _______________|___________________
                   |               |                   |
            2    2(2)5           6(4)11             12(7)21
                   |               ^                   ^
            3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                                  |          |
            4                                  14(9)15   18(11)19

        level               Move 8 - > 5
            1                     1(1)22
                     _______________|__________________
                    |               |                  |
            2     2(2)5           6(4)15            16(7)21
                    |               ^                  |
            3     3(3)4      7(5)12   13(6)14      17(10)20
                               |                        |
            4                8(8)11                18(11)19
                               |
            5                9(9)10
Перенос ноды по дереву или между деревьев:

Move inside (перемещает ноду на первое место от родителя):
node = session.query(Tree).filter(Tree.id == 8).one()
node = session.query(Tree).filter(Tree.id == 4).one()
node.move_inside("15")
получим
                 4 -> 15
        level           Nested sets tree1
        1                    1(1)16
                _______________|_____________________
               |                                     |
        2    2(2)5                                 6(7)15
               |                                     ^
        3    3(3)4                            7(8)10   11(10)14
                                                |          |
        4                                     8(9)9    12(11)13

        level           Nested sets tree2
        1                     1(12)28
                 ________________|_______________________
                |                |                       |
        2    2(13)5            6(15)17                18(18)27
               |                 ^                        ^
        3    3(14)4    7(4)12 13(16)14  15(17)16  19(19)22  23(21)26
                         ^                            |         |
        4          8(5)9  10(6)11                 20(20)21  24(22)25
Move after (перемещает ноду после текущей ноды):
node = session.query(Tree).filter(Tree.id == 8).one()
node = session.query(Tree).filter(Tree.id == 8).one()
node.move_after("5")
получим
       level           Nested sets example
            1                    1(1)22
                    _______________|___________________
                   |               |                   |
            2    2(2)5           6(4)11             12(7)21
                   |               ^                   ^
            3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                                  |          |
            4                                  14(9)15   18(11)19

        level               Move 8 after 5
            1                     1(1)22
                     _______________|__________________
                    |               |                  |
            2     2(2)5           6(4)15            16(7)21
                    |               ^                  |
            3     3(3)4    7(5)8  9(8)12  13(6)14   17(10)20
                                    |                  |
            4                    10(9)11            18(11)19
Move to top level (выделение ноды в самостоятельное дерево):
node = session.query(Tree).filter(Tree.id == 8).one()
node = session.query(Tree).filter(Tree.id == 15).one()
node.move_after("1")
получим
        level           tree_id = 1
        1                    1(1)22
                _______________|___________________
               |               |                   |
        2    2(2)5           6(4)11             12(7)21
               |               ^                   ^
        3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                              |          |
        4                                  14(9)15   18(11)19

        level           tree_id = 2
        1                     1(15)6
                                 ^
        2                 2(16)3   4(17)5

        level           tree_id = 3
        1                    1(12)16
                 _______________|
                |               |
        2    2(13)5          6(18)15
                |               ^
        3    3(14)4     7(19)10   11(21)14
                           |          |
        4               8(20)9    12(22)13

За основу был взят пример Mike Bayer, впринципе в тестах можно посмотреть больше примеров. Ссылка на github: https://github.com/ITCase/sqlalchemy_mptt

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. Но по аналогии можно поднять любой другой проект. Думаю вам эта статья поможет, хотя бы начать писать тесты :)