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

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.

09 марта 2013

CRUD интерфейс для SQLAlchemy и подключение к Pyramid

Запилил Yet another CRUD интерфейс для SQLAlchemy. По сути это аналог Django админки или FormAlchemy, но ОЧЕНЬ сильно упрощенный, ничего лишнего. Есть поддержка большинства полей + кастомные поля типа файл(для загрузки файлов, изображений) и GUID. Довольно просто подключить к Pyramid проекту и сразу начать работать по адресу http://localhost:6543/sacrud

Проект доступен на github https://github.com/uralbash/sacrud

В след. релизах планирую добавить новые типы полей, кастомные поля типа tree и btree с AJAX обработкой в интерфейсе, расширение для других фреймворков (например flask), кастомные фильтры, пагинацию итд


Установка

PyPi

pip install sacrud

Из исходников

python setup.py install

 

Пример использования в Pyramid

Add to your project config:
# pyramid_jinja2 configuration
config.include('pyramid_jinja2')
config.add_jinja2_search_path("myprojectname:templates")

from .models import (Model1, Model2, Model3,)
# add sacrud and project models
config.include('sacrud.pyramid_ext')
settings = config.registry.settings
settings['sacrud_models'] = (Model1, Model2, Model3)
go to http://localhost:6543/sacrud

Скриншоты

список таблиц

ScreenShot

список записей в таблице

ScreenShot

редактирование записи

ScreenShot

13 ноября 2011

Pylons + FormAlchemy REST Controller

Для своих REST контроллеров можно использовать, формы FormAlchemy.
Создаем контроллер:
yourproj% paster restcontroller comment comments
Creating yourproj/yourproj/controllers/comments.py
Creating yourproj/yourproj/tests/functional/test_comments.py
Или если нужно в отдельной директории
yourproj% paster restcontroller admin/tracback admin/trackbacks
Creating yourproj/controllers/admin
Creating yourproj/yourproj/controllers/admin/trackbacks.py
Creating yourproj/yourproj/tests/functional/test_admin_trackbacks.py

В файле нашего REST контроллера добавим
from formalchemy.ext.pylons.controller import RESTController

И в конце файла обернем его так
# wrap with formalchemy RESTController
CommentsController = RESTController(CommentsController, 'comment', 'comments')

Теперь если закомментировать какой-нибудь из стандартных методов в контроллере(index, new, update, delete, show, edit) он будет браться из контроллера FormAlchemy со стандартными формами. Довольно удобно в разработке.