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

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

10 марта 2014

Обновление sacrud. Версия 0.1.2

Код: https://github.com/uralbash/sacrud
Описание: http://sacrud.readthedocs.org/ (в процессе)

В этой версии делался упор на кастомизацию интерфейса.

Что нового?
* добавлена пагинация
* теперь pk показывается по умолчанию в форме создания/редактирования
* новая опция sacrud_detail_col где можно задать отображаемые поля в форме редактирования
* новая опция sacrud_list_col где можно задать отображаемые поля в списке записей
* новая опция verbose_name для полей и таблиц
* опция sacrud_css_class, назначает CSS стили полям
* новый атрибут колонки sacrud_position: "inline" (см. реализацию horizontal_fields)
* новая функция horizontal_fields
* новый тип exttype.GUID
* для переопределения base.html создан шаблон redefineme.html
* вывод флеш уведомлений если задана sesion_factory
* исправлены названия классов в шаблонах у полей
* исправлен шаблон ForeignKey.jinja2
* sa_create, sa_read, sa_update, sa_delete view вынесены в общий класс CRUD
* в example добавлены pyramid_beaker , примеры с кастомизацией, FileField и все остальные поля которые есть в sacrud/templates/sacrud/types.

 Пример кастомизации:
models.py

class TestCustomizing(Base):
    __tablename__ = "test_customizing"

    id = Column(Integer, primary_key=True)
    name = Column(String)
    date = Column(Date, info={"verbose_name": 'date JQuery-ui'})
    name_ru = Column(String, info={"verbose_name": u'Название', })
    name_fr = Column(String, info={"verbose_name": u'nom', })
    name_bg = Column(String, info={"verbose_name": u'Име', })
    name_cze = Column(String, info={"verbose_name": u'název', })
    description = Column(Text)
    description2 = Column(Text)

    visible = Column(Boolean)
    in_menu = Column(Boolean, info={"verbose_name": u'menu?', })
    in_banner = Column(Boolean, info={"verbose_name": u'on banner?', })

    # SACRUD
    verbose_name = u'Customizing table'
    sacrud_css_class = {'tinymce': [description, description2],
                        'content': [description],
                        'name': [name], 'Date': [date]}
    sacrud_list_col = [name, name_ru, name_cze]
    sacrud_detail_col = [name,
                         hosrizontal_field(name_ru, name_bg, name_fr, name_cze,
                                           sacrud_name=u"i18n names"),
                         description, date,
                         hosrizontal_field(in_menu, visible, in_banner,
                                           sacrud_name=u"Расположение"),
                         description2]

templates/sacrud/redefineme.jinja2
{% extends "sacrud/base.jinja2" %}

{% block userspace %}
  {{ super() }}

  <!-- Date field -->
  <script src="http://code.jquery.com/jquery-1.10.2.js"></script>
  <script src="http://code.jquery.com/ui/1.10.4/jquery-ui.js"></script>

  <link href="//code.jquery.com/ui/1.10.4/themes/smoothness/jquery-ui.css" rel="stylesheet"></link>
  <style>
    .content {
      height: 550px;
    }
    .name {
      width: 400px;
    }
  </style>

  <script>
    $(function() {
      $(".Date").datepicker({ dateFormat: 'yy-mm-dd' });
    });
  </script>

  <script src="//tinymce.cachefly.net/4.0/tinymce.min.js"></script>
  <script>
    tinymce.init({
      selector:'textarea.tinymce',
      plugins: "image link",
      file_browser_callback: function(field_name, url, type, win) {
        tinymce.activeEditor.windowManager.open({
            title: "SACRUD file browser",
            url: "/image/filebrowser",
            width: 600,
            height: 400,
          }, {
            oninsert: function(url) {
              win.document.getElementById(field_name).value = url;
            }
        });
      },
    });
  </script>
{% endblock %}

{% block sa_body %}
  {{ super() }}
{% endblock %}

Результат:
Пример кастомного CRUD интерфейса для SQLAlchemy

23 февраля 2014

Обновление sacrud. Версия 0.1.1

  
Код: https://github.com/uralbash/sacrud
Описание: http://sacrud.readthedocs.org/

Что нового?
* в расширении для pyramid параметр "sacrud_models" переименован в "sacrud.models"
* "sacrud.models" теперь словарь, а не список:
    settings['sacrud.models'] = {
        'Company': [Company, User, EmployeeType],
        'Auth': [Group, GroupPermission, UserGroup,
        GroupResourcePermission, Resource, UserPermission,
        UserResourcePermission, ExternalIdentity],
        '': [Company]
    }
* в словаре можно поделить модели на группы
* тесты исправлены для Pyramid 1.5:
Removed the ability to influence and query a pyramid.request.Request object as if it were a dictionary. Previously it was possible to use methods like __getitem__, get, items, and other dictlike methods to access values in the WSGI environment. This behavior had been deprecated since Pyramid 1.1. Use methods of request.environ (a real dictionary) instead.

19 ноября 2013

sacrud 0.1.0

Обновил sacrud до 0.1.0


Изменения:
* исправлено куча ошибок
* добавлена документация

В планах система расширения и кастомизации.

06 сентября 2013

Новое в sacrud 0.1.0a

Теперь sacrud 0.1.0a
Что нового:
* dnd через любое поле
* объединение полей
* js сортировка колонок
Приложение для ознакомления pyramid_sacrud_example
В планах:
* yapsy плагины
* mass delete
* ImageField в pyramid_sacrud_example

26 августа 2013

Запись в БД через sacrud используя SQLAlchemy session.

Для простых CRUD действий с БД, можно воспользоваться модулем action из sacrud. Это немного сократит код и добавит некоторой универсальности в ПО со сложной логикой.
from sacrud import action
from models import (
    DBSession,
    TestTable,
)

hstore_data = str({'param1': 'bla bla bla',
            'param2': 'bla bla bla2',
            'param3': '7389a498-9347-48e3-835d-c3900dcd2566',
            'patam4': 'dddddddd'})

param = {'value': ('123',),
         'description': ('test description',),
         'myhash': [hstore_data, ],
        }
# записывает транзакцию в БД
action.create(DBSession, TestTable, param)

параметры в виде списка сделаны для того что бы можно было принимать множественные значения поля с HTML формы.

UPD: в новой версии можно делать так:
param = {'value': '123',
         'description': 'test description',
         'myhash': hstore_data,
        }
# записывает транзакцию в БД
action.create(DBSession, TestTable, param)

Обновление sacrud и pyramid_ext

Новая версия sacrud 0.0.3. В ней поправлены некоторые баги, добавлены нескучные обои элементы дизайна в расширении для Pyramid и создан отдельный репозитарий с примерами работы разных типов полей(pyramid_sacrud_example). Pyramid_sacrud_example работает только с Postgres потому что включает в себя примеры полей специфичных именно для этой БД(таких как hstore).