Содержание


Введение в Pyjamas

Часть 1. Используем объединенную мощь GWT и Python

Comments

Серия контента:

Этот контент является частью # из серии # статей: Введение в Pyjamas

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Введение в Pyjamas

Следите за выходом новых статей этой серии.

Инфраструктура Google Web Toolkit (GWT) позволяет разрабатывать RIA-приложения (Rich Internet Application - Интернет-приложение с богатой функциональностью) с поддержкой Ajax, используя исключительно код Java™. При разработке приложений можно пользоваться всем богатством инструментария Java (среды разработки, рефакторинг, автодополнение кода, отладчики и т.д.), а само приложение будет работать в большинстве Web-браузеров. С помощью GWT можно создавать приложения, выглядящие как обычные настольные приложения, но работающие в браузере. Pyjamas - это порт GWT на Python, который предоставляет инфраструктуру для разработки Ajax-приложений на языке Python.

В состав Pyjamas входят отдельный компилятор Python в JavaScript, инфраструктура для работы с Ajax и набор виджетов. Эти компоненты позволяют создавать полноценные графические web-приложения без написания кода JavaScript.

В этой статье мы рассмотрим причины и историю появления Pyjamas. Также на примере создания простого приложения для хранения контактной информации (имя, адрес электронной почты, телефонный номер) мы рассмотрим преимущества использования Pyjamas и инструменты разработки. Код данного приложения доступен для загрузки.

Во второй статье этого цикла мы расскажем о создании пользовательских компонентов в Pyjamas.

История

Python был одним из первых популярных языков, перенесенных на JVM (Jython), а потом и на .Net (IronPython). Эти языки позволяют писать программы, используя синтаксис в стиле Python, которые транслируются в машинный код, аналогичный тому, что генерируется при написании программ на C (Cython). Поэтому неудивительно, что Python стал одним из первых языков (после того как Google проторил эту дорогу с помощью Java), для которых была реализована трансляция кода в код JavaScript для создания кросс-браузерных web-приложений.

В недалеком прошлом возможность сделать приложение исключительно на основе технологии Ajax была малореальной. Однако теперь с помощью GWT можно разрабатывать RIA-приложения с поддержкой Ajax полностью на Java. GWT позволяет писать приложения, которые ведут себя как настольные приложения, но работают в браузере.

Пакеты Adobe AIR и Silverlight, наоборот, позволяют запускать на локальной машине приложения, стилизованные под Web. WebKit используется для вывода на экран в таких продуктах, как Android, Adobe AIR, Google Chrome, Safari и iPhone. Однако GWT не позволяет писать приложения, выполняющиеся как настольные приложения (несмотря на то, что набор графических инструментов GWT основан на WebKit).

В Pyjamas есть компилятор, похожий на тот, что имеется в GWT, и набор виджетов Ajax, предоставляющих тот же API, что их аналоги в GWT (фактически при разработке приложений с помощью Pyjamas можно пользоваться документацией GWT). Python обладает лаконичным и мощным синтаксисом; например, код GWT 1.2 состоит из 80 000 строк, а в Pyjamas для реализации той же функциональности потребовалось лишь 8 000 строк.

Обзор Pyjamas

WebKit, XUL и их собратья подарили настольным приложениям современный стиль. Pyjamas подарил WebKit разработчикам на Python. Благодаря Webkit Pyjamas становится кросс-браузерным и кросс-платформенным набором GUI-виджетов. На нем можно разрабатывать виджеты, которые будут работать везде, где работают WebKit и XUL. Приложения на основе API Pyjamas будут работать везде, где будет работать GWT. Помимо этого, Pyjamas позволяет создавать поверх WebKit и XUL настольные приложения. Этот способ создания приложений предпочтительнее, чем использование Qt и GTK, потому что WebKit поддерживает CSS и используется для передачи графики во многих других продуктах (iPhone, Safari, Android и т.д.). Однако использование XUL и WebKit в Python сопряжено с некоторыми проблемами (см. врезку).

Pyjamas, как и GWT, является инфраструктурой GUI-компонентов. Если вы работали с Swing или GWT, то разработка в Pyjamas покажется вам знакомой. Как и большинство GUI-инфраструктур, Pyjamas работает на основе событий.

В Pyjamas сначала обычно создаются контейнеры, на которые затем добавляются виджеты. Примерами виджетов являются надписи, текстовые поля, кнопки и т.д. Виджеты, например, кнопки, имеют обработчики событий, которые позволяют отлавливать различные события, например, нажатие на кнопку.

Разработка приложений с помощью Pyjamas - это несложное дело, благодаря тому, что можно использовать традиционные отладочные инструменты Python, например unit-тесты, инструкции print, отладчик Python (pdb, работающий в командной строке). Можно даже использовать для отладки имеющуюся в Eclipse поддержку Python. Помните, что приложения Pyjamas можно выполнять как обычные Python-приложения; их не обязательно транслировать в код JavaScript. С Pyjamas можно работать так же, как с любым другим набором инструментов Python для разработки GUI.

Первая версия GUI-интерфейса приложения, которое мы рассмотрим в качестве примера в данной статье, была разработана исключительно с помощью командной строки интерпретатора Python. Изначально эта версия даже не разворачивалась на Web-сервере, а выполнялась как настольное приложение. Выполнение в виде настольного приложения - очень полезная возможность при разработке RIA-приложений, так как это существенно облегчает отладку программы.

При развертывании приложения в Web следует внимательно отнестись к выбору подключаемых библиотек. Обычно в выполняющихся в браузере приложениях Pyjamas используются сервисы JSON-RPC (JavaScript Object Notation).

Зависимости

Для создания данного примера приложения необходимо скачать и установить Pyjamas. Это не самая простая задача. Сначала я безуспешно пытался запустить Pyjamas на Ubuntu, потом бросил это дело и установил ее на Debian. (Говорят, Pyjamas неплохо работает на Windows®.) Установленная на Debian версия работала отлично. Возможно, процесс установки в ближайшее время будет изменяться, поэтому следуйте самым свежим инструкциям по установке Pyjamas на вашу систему, которые можно найти на сайте Pyjamas (см. Ресурсы).

В работе нашего приложения используются MySQL, Apache, mod_python и JSON-RPC для Python.

Создаем простое приложение

Наше простое приложение для управления контактами хранит базовую контактную информацию, такую как имя, адрес электронной почты и телефонный номер. Мы начнем с простейшего CRUD-приложения, позволяющего создавать (Create), читать (Read), обновлять (Update) и удалять (Delete) контакты, а затем добавим в него механизм для хранения данных. Все приложение можно реализовать в виде одного простого сценария Python, в котором "база данных" будет храниться в оперативной памяти. В данном примере используется уровень сервиса, который изначально работает только с оперативной памятью; позднее мы заменим его сервисом на основе JSON, который будет хранить контакты в реляционной базе данных MySQL.

Чтобы написать сервис-заглушку, необходимо понять, как приложение будет работать во время выполнения. Приложение будет делать асинхронные вызовы к сервису JSON. Когда мы скомпилируем Pyjamas-приложение в RIA-приложение (т.е. в разметку HTML и код JavaScript), вызовы Ajax будут возвращать результаты асинхронно. Поэтому при создании сервиса-заглушки мы будем делать асинхронные вызовы методов GUI. В листинге 1 показано, как сервис ContactService работает с GUI с помощью методов обратного вызова, которые мы рассмотрим позже. Таким образом эмулируется асинхронная работа сервиса JSON, который мы добавим немного позднее.

Листинг 1. Сервис-заглушка ContactService
class Contact:
    def __init__(self, name="", email="", phone=""):
        self.name = name
        self.email = email
        self.phone = phone

class ContactService:
    def __init__(self, callback):
        self.callback = callback
        self.contacts = []

    def addContact(self, contact):
        self.contacts.append(contact)
        self.callback.service_eventAddContactSuccessful()
    
    def updateContact(self, contact):
        self.callback.service_eventUpdateContactSuccessful()

    def removeContact(self, contact):
        self.contacts.remove(contact)
        self.callback.service_eventRemoveContactSuccessful()
        
    def listContacts(self):
        self.callback.service_eventListRetrievedFromService(self.contacts)

Класс Contact представляет собой запись данных о контакте (далее - контакт) (имя, адрес электронной почты, телефонный номер). Класс ContactService хранит список контактов в оперативной памяти (не на диске). Этот простой класс позволит нам разработать GUI и логику отображения данных, а затем, сделав лишь небольшие изменения, протестировать его с настоящим сервисом JSON.

Класс ContactService оповещает класс ContactListGUI (определенный в листинге 2) о событиях сервиса с помощью методов, начинающихся с префикса service_eventXXX.

ContactListGUI - это довольно простой класс, состоящий из 125 строк кода, который управляет девятью GUI-виджетами. Также он взаимодействует с классом ContactService для управления CRUD-операциями с контактами, как показано в листинге 2.

Листинг 2. ContactListGUI
import pyjd # этот модуль нужен для трансляции в JavaScript
from pyjamas.ui.RootPanel import RootPanel
from pyjamas.ui.Button import Button
from pyjamas.ui.Label import Label
from pyjamas import Window

from  pyjamas.ui.Grid import Grid
from  pyjamas.ui.Hyperlink import Hyperlink
from  pyjamas.ui.TextBox import TextBox

# Константы
CONTACT_LISTING_ROOT_PANEL = "contactListing"
CONTACT_FORM_ROOT_PANEL = "contactForm"
CONTACT_STATUS_ROOT_PANEL = "contactStatus"
CONTACT_TOOL_BAR_ROOT_PANEL = "contactToolBar"
EDIT_LINK = 3
REMOVE_LINK = 4

#Код сервиса пропущен

class ContactListGUI:

    def __init__(self):
        self.contactService = ContactService(self)
        self.currentContact = Contact("Rick", "rhightower@gmail.com", "555-555-5555")
        self.addButton = Button("Add contact", self.gui_eventAddButtonClicked)
        self.addNewButton = Button("Add new contact", self.gui_eventAddNewButtonClicked)
        self.updateButton = Button("Update contact", self.gui_eventUpdateButtonClicked)

        self.nameField = TextBox()
        self.emailField = TextBox()
        self.phoneField = TextBox()
        self.status = Label()
        self.contactGrid = Grid(2,5)
        self.contactGrid.addTableListener(self)

        self.buildForm()
        self.placeWidgets()
        self.contactService.listContacts()	

    
    def onCellClicked(self, sender, row, cell):
        print "sender=%s row=%s cell=%s" % (sender, row, cell)
        self.gui_eventContactGridClicked(row, cell)

    def onClick(self, sender):
        if sender == self.addButton:
            self.gui_eventAddButtonClicked()
        elif sender == self.addNewButton:
            self.gui_eventAddNewButtonClicked()
        elif sender == self.updateButton:
            self.gui_eventUpdateButtonClicked()
                
    def buildForm(self):
        formGrid = Grid(4,3)
        formGrid.setVisible(False)
        
        formGrid.setWidget(0, 0, Label("Name"))
        formGrid.setWidget(0, 1, self.nameField);

        formGrid.setWidget(1, 0, Label("email"))
        formGrid.setWidget(1, 1, self.emailField)
        
        formGrid.setWidget(2, 0, Label("phone"))
        formGrid.setWidget(2, 1, self.phoneField)
        
        formGrid.setWidget(3, 0, self.updateButton)
        formGrid.setWidget(3, 1, self.addButton)

        self.formGrid = formGrid
        
    def placeWidgets(self):
        RootPanel(CONTACT_LISTING_ROOT_PANEL).add(self.contactGrid)
        RootPanel(CONTACT_FORM_ROOT_PANEL).add(self.formGrid)
        RootPanel(CONTACT_STATUS_ROOT_PANEL).add(self.status)
        RootPanel(CONTACT_TOOL_BAR_ROOT_PANEL).add(self.addNewButton)

    def loadForm(self, contact):
        self.formGrid.setVisible(True)
        self.currentContact = contact
        self.emailField.setText(contact.email)
        self.phoneField.setText(contact.phone)
        self.nameField.setText(contact.name)
    
    def copyFieldDateToContact(self):
        self.currentContact.email = self.emailField.getText()
        self.currentContact.name = self.nameField.getText()
        self.currentContact.phone = self.phoneField.getText()

Метод init класса ContactListGUI создает с помощью метода buildForm новую сетку и помещает на нее поля для редактирования данных контакта. Затем в методе init вызывается метод placeWidgets, который размещает виджеты contactGrid, formGrid, status и addNewButton в слотах, определенных на HTML-странице нашего GUI-приложения. Разметка HTML-страницы приложения показана в листинге 3.

На рисунке 1 представлены виджеты, используемые в нашем приложении управления контактами.

Рисунок 1. Виджеты GUI-интерфейса для управления контактами
Screen showing                     contacts, phone numbers, e-mails and buttons to add or update                     contacts.
Screen showing contacts, phone numbers, e-mails and buttons to add or update contacts.
Листинг 3. Обработчики событий класса ContactListGUI
<html>
    <head>
      <meta name="pygwt:module" content="Contacts">
      <link rel='stylesheet' href='Contacts.css'>
      <title>Contacts</title>
    </head>
    <body bgcolor="white">

      <script language="javascript" src="bootstrap.js"></script>

      <h1>Contact List Example</h1>

      <table align="center">
      <tr>
        <td id="contactStatus"></td> 
      </tr>
      <tr>
        <td id="contactToolBar"></td>
      </tr>
      <tr>
        <td id="contactForm"></td>
      </tr>
      <tr>
        <td id="contactListing"></td>
      </tr>
      </table>
    </body>
</html>

Константы (такие как CONTACT_LISTING_ROOT_PANEL="contactListing") соответствуют идентификаторам элементов, определенных на HTML-странице. Это позволяет разработчику страницы лучше контролировать разметку виджетов приложения.

Простое приложение создано. В следующем разделе мы рассмотрим пару типичных сценариев его использования.

Показываем список при загрузке страницы

При первой загрузке приложения вызывается метод __init__ класса ContactListEntryPoint. В методе __init__ вызывается метод listContacts класса ContactServiceDelegate, в котором асинхронно вызывается сервисный метод listContact. Как показано в листинге 4, метод listContact сервиса-заглушки ContactService вызывает метод обработчика сервисного события service_eventListRetrievedFromService.

Листинг 4. ContactListGUI: вызов обработчика события listContact
class ContactListGUI:
    …
    def service_eventListRetrievedFromService(self, results):
        self.status.setText("Retrieved contact list")
        self.contacts = results;
        self.contactGrid.clear();
        self.contactGrid.resizeRows(len(self.contacts))
        row = 0
        
        for contact in results:
            self.contactGrid.setWidget(row, 0, Label(contact.name))
            self.contactGrid.setWidget(row, 1, Label (contact.phone))
            self.contactGrid.setWidget(row, 2, Label (contact.email))
            self.contactGrid.setWidget(row, EDIT_LINK, Hyperlink("Edit", None))
            self.contactGrid.setWidget(row, REMOVE_LINK, Hyperlink("Remove", None))
            row += 1

Метод service_eventListRetrievedFromService сохраняет полученный от сервера список контактов. Затем он:

  • Очищает элемент contactGrid, в котором отображается список контактов.
  • Изменяет количество строк в соответствии с размером списка контактов, полученного от сервера.
  • Обходит список контактов и помещает имя телефон и адрес электронной почты в первые три столбца соответствующей контакту строки.
  • Отображает для каждого контакта ссылки Edit и Remove, предоставляя пользователям удобный способ удаления и редактирования контактов.

Редактируем существующий контакт

При нажатии в списке контактов ссылки Edit вызывается метод gui_eventContactGridClicked, который показан в листинге 5.

Листинг 5. Метод-обработчик gui_eventContactGridClicked из класса ContactListGUI
class ContactListGUI:

    …
    def gui_eventContactGridClicked(self, row, col):
         contact = self.contacts[row]
         self.status.setText("Name was " + contact.name + " clicked ")
         if col==EDIT_LINK:
             self.addNewButton.setVisible(False)
             self.updateButton.setVisible(True)
             self.addButton.setVisible(False)
             self.loadForm(contact)
         elif (col==REMOVE_LINK):
             self.contactService.removeContact(contact)

    …
    def loadForm(self, contact):
        self.formGrid.setVisible(True)
        self.currentContact = contact
        self.emailField.setText(contact.email)
        self.phoneField.setText(contact.phone)
        self.nameField.setText(contact.name)

Метод gui_eventContactGridClicked определяет, какая из ссылок - Edit или Remove - была нажата. Затем он скрывает элементы addNewButton и addButton и делает видимым элемент updateButton. Кнопка updateButton отображает элемент formGrid и позволяет пользователю послать сервису ContactService обновленную информацию. Затем в методе gui_eventContactGridClicked вызывается метод loadForm (показанный в листинге 5), который:

  • Делает видимым элемент formGrid.
  • Задает в качестве текущего контакта редактируемую запись.
  • Копирует атрибуты контакта в виджеты emailField, phoneField и nameField.

Когда пользователь нажимает кнопку Update, вызывается метод-обработчик gui_eventUpdateButtonClicked, который показан в листинге 6. Этот метод:

  • Делает видимой кнопку addNewButton, позволяющую пользователю добавлять новые контакты.
  • Скрывает решетку formGrid.
  • Вызывает метод copyFieldDateToContact, который копирует текст из виджетов emailField, phoneField и nameField обратно в свойства контакта currentContact.
  • Вызывает метод updateContact класса ContactServiceDelegate, чтобы передать сервису обновленные данные контакта.
Листинг 6. Метод-обработчик gui_eventUpdateButtonClicked класса ContactListGUI
class ContactListGUI:

    …

    def gui_eventUpdateButtonClicked(self, sender):
        self.addNewButton.setVisible(True)
        self.formGrid.setVisible(False)
        self.copyFieldDateToContact()
        self.contactService.updateContact(self.currentContact)

    def copyFieldDateToContact(self):
        self.currentContact.email = self.emailField.getText()
        self.currentContact.name = self.nameField.getText()
        self.currentContact.phone = self.phoneField.getText()

Два приведенных выше сценария иллюстрируют работу приложения и его работу с графикой с помощью инфраструктуры, предоставляемой платформой App Engine для Java. Остальные методы-обработчики событий GUI, имеющиеся в классе ContactListGUI, показаны в листинге 7, а остальные методы обратного вызова сервиса показаны в листинге 8.

Листинг 7. Метод-обработчик gui_eventUpdateButtonClicked класса ContactListGUI
class ContactListGUI:

    …
    def gui_eventContactGridClicked(self, row, col):
         contact = self.contacts[row]
         self.status.setText("Name was " + contact.name + " clicked ")
         if col==EDIT_LINK:
             self.addNewButton.setVisible(False)
             self.updateButton.setVisible(True)
             self.addButton.setVisible(False)
             self.loadForm(contact)
         elif (col==REMOVE_LINK):
             self.contactService.removeContact(contact)


    def gui_eventAddButtonClicked(self, sender):
        self.addNewButton.setVisible(True)
        self.formGrid.setVisible(False)
        self.copyFieldDateToContact()
        self.contactService.addContact(self.currentContact)

    def gui_eventUpdateButtonClicked(self, sender):
        self.addNewButton.setVisible(True)
        self.formGrid.setVisible(False)
        self.copyFieldDateToContact()
        self.contactService.updateContact(self.currentContact)


    def gui_eventAddNewButtonClicked(self, sender):
        self.addNewButton.setVisible(False)
        self.updateButton.setVisible(False)
        self.addButton.setVisible(True)
        self.loadForm(Contact())
Листинг 8. Методы обратного вызова сервиса ContactListGUI
class ContactListGUI:
    …
    def service_eventListRetrievedFromService(self, results):
        self.status.setText("Retrieved contact list")
        self.contacts = results;
        self.contactGrid.clear();
        self.contactGrid.resizeRows(len(self.contacts))
        row = 0
        
        for contact in results:
            self.contactGrid.setWidget(row, 0, Label(contact.name))
            self.contactGrid.setWidget(row, 1, Label (contact.phone))
            self.contactGrid.setWidget(row, 2, Label (contact.email))
            self.contactGrid.setWidget(row, EDIT_LINK, Hyperlink("Edit", None))
            self.contactGrid.setWidget(row, REMOVE_LINK, Hyperlink("Remove", None))
            row += 1

    def service_eventAddContactSuccessful(self):
        self.status.setText("Contact was successfully added")
        self.contactService.listContacts()

    def service_eventUpdateContactSuccessful(self):
        self.status.setText("Contact was successfully updated")
        self.contactService.listContacts()

    def service_eventRemoveContactSuccessful(self):
        self.status.setText("Contact was removed")
        self.contactService.listContacts()

Компилируем пример приложения

Наш пример приложения можно скомпилировать и запустить в любом современном браузере. Однако работающее в браузере RIA-приложение неудобно отлаживать. К счастью, с помощью Pyjamas-Desktop наше приложение можно выполнять как обычное Python-приложение, как показано в листинге 9.

Листинг 9. Запускаем Pyjamas-Desktop
import pyjd # этот модуль нужен для трансляции в JavaScript
...
if __name__ == '__main__':
    pyjd.setup("public/Contacts.html")
    contacts = ContactListGUI()
pyjd.run()

Показанный в листинге 9 код создает экземпляр настольного приложения Python и затем вызовом метода run запускает рабочий стол. Когда приложение запущено таким образом, его можно отлаживать с помощью pdb или любой среды разработки для Python, поддерживающей графический режим отладки.

Я установил Pyjamas в директорию tools, находящуюся в моей домашней директории. Для использования отладчика Python следует добавить в вашу переменную среды PYTHONPATH пути к библиотекам Pyjamas и Pyjamas-Desktop, как показано в листинге 10.

Листинг 10. Добавляем Pyjamas в переменную PYTHONPATH
export PYTHONPATH=/home/rick/tools/pyjamas:/home/rick/tools/pyjamas/library

Теперь приложение готово и мы можем запустить pyjsbuild, чтобы скомпилировать приложение в HTML, JavaScript и JSON-RPC. В листинге 11 показан пример сценария для запуска pyjsbuild.

Листинг 11. build.sh
#!/bin/sh

options="$*"
#if [ -z $options ] ; then options="-O";fi
~/tools/pyjamas/bin/pyjsbuild --print-statements $options Contacts.py

После компиляции приложения нам остается только настроить Web-сервер для работы со сгенерированной директорией /output. Данный пример работал в свежей версии Debian, поэтому я с помощью apt-get установил apache2 и mod_python, как показано в листинге 12.

Листинг 12. Устанавливаем apache2 и mod_python
$sudo apt-get install apache2 libapache2-mod-python

В следующей версии нашего приложения мы будем использовать модуль mod_python. Пример данного приложения находится в директории /home/rick/tools/pyjamas/examples/contact1. Чтобы настроить сервер Apache на работу с этой директорией, добавьте в файл httpd.conf следующие строки (в Debian этот файл находится в директории /etc/apache2).

Листинг 13. /etc/apache2/httpd.conf
Alias /pj "/home/rick/tools/pyjamas" 
<Directory "/home/rick/tools/pyjamas">
    Options Indexes FollowSymLinks MultiViews
    AllowOverride None
    Order deny,allow
    allow from all
</Directory>

Добавляем поддержку JSON-RPC

После завершения работы над логикой GUI пора начинать разработку сервиса JSON-RPC, который мы также реализуем на Python. JSON-RPC - это стандарт, для реализации которого на серверной стороне можно использовать любой язык программирования. Таким образом, графический интерфейс Pyjamas можно интегрировать в существующие проекты, имеющие Web-сервис JSON-RPC. JSON - это формат обмена данными. В нем используются две структуры данных:

  • Коллекции пар ключ/значение (словарь в Python, хэш-таблица в Java или ассоциативный массив в Perl)
  • Массивы

JSON-RPC - это протокол удаленного вызова процедур, который использует JSON для кодирования и сериализации аргументов и возвращаемых значений. JSON-RPC имеет привязку к Python. Twisted, Django и многие другие инфраструктуры Python также поддерживают JSON-RPC. В листинге 14 показан простой способ установки JSON-RPC.

Листинг 14. Устанавливаем JSON-RPC
$ svn checkout 
   http://svn.json-rpc.org/trunk/python-jsonrpc

$ cd python-jsonrpc
$ python setup.py install

Для написания сервиса JSON-RPC следует декорировать методы, которые вы хотите использовать в сервисе, декоратором @ServiceMethod, а затем создать в модуле переменную с именем service, указывающую на экземпляр класса, который мы хотим сделать доступным через JSON-RPC. В листинге 15 показан пример.

Листинг 15. ContactService: сервис JSON-RPC для списка контактов
import logging

logging.basicConfig(filename="/tmp/contactjson.log",
                    level=logging.DEBUG)


logging.debug("Loading contact service")

from jsonrpc import ServiceMethod

use_mysql=True

if use_mysql:
    import MySQLdb as db_api
    logging.debug("Using mysql")
else:
    import sqlite3 as db_api
    logging.debug("Using sqllite3")


db_url = "/tmp/contacts"


class ContactService:

    @ServiceMethod
    def test(self):
        logging.info("Test called")
        return "test"

    def connection(self):
        if use_mysql:
            connection =  db_api.connect(passwd="mypassword", db="contactdb", user="root")
        else:
            connection =  db_api.connect(db_url)
        return connection

    def run_update(self, func):
        
        connection = self.connection()
        cursor = connection.cursor()
        try:
            func(cursor)
            cursor.close()
            connection.commit()
        except Exception, e:
            connection.rollback()
            logging.exception("problem handling update")
            raise e
        finally:
            connection.close()

    def run_query(self, func):
        connection = self.connection()
        cursor = connection.cursor()
        lst = None
        try:
            func(cursor)
            lst = cursor.fetchall()
            cursor.close()
        except Exception, e:
            logging.exception("problem handling query")
            raise e
        finally:
            connection.close()
        return lst

    @ServiceMethod
    def addContact(self, contact):
        logging.debug("Add contact called %s", `contact`)
        def ac(cursor):
            if use_mysql:
                cursor.execute(""" 
                  insert into contact 
                          (name, phone, email) 
                  values (%(name)s, %(phone)s, %(email)s) 
                  """, contact)
            else:
                cursor.execute(""" 
                  insert into contact 
                          (id, name, phone, email) 
                  values (NULL, :name, :phone, :email) 
                  """, contact)
        self.run_update(ac)


    @ServiceMethod
    def updateContact(self, contact):
       logging.debug("Update contact called %s", `contact`)
       def uc(cursor):
           if use_mysql:
               cursor.execute(""" 
                  update contact 
                          set name = %(name)s, email = %(email)s, phone = %(phone)s
                  where id=%(id)s;
                  """, contact)
           else:
               cursor.execute(""" 
                  update contact 
                          set name = :name, email = :email, phone = :phone
                  where id=:id;
                  """, contact)

       self.run_update(uc)


    @ServiceMethod
    def removeContact(self, contact):
       logging.debug("Remove contact called %s", `contact`)
       def uc(cursor):
           if use_mysql:
               cursor.execute("delete from contact where id=%(id)s;", contact)
           else:
               cursor.execute("delete from contact where id=:id;", contact)
       self.run_update(uc)

        
    @ServiceMethod
    def listContacts(self):
        logging.debug("list contact called")
        def lc(cursor):
            cursor.execute("select name, phone, email, id from contact")
        lst = self.run_query(lc)
        def toMap(x):
            return {"name":x[0],"phone": x[1], "email":x[2], "id":x[3]}
        return map(toMap, lst)


service = ContactService()

#Если вы не смогли заставить работать mod_python,
# вы можете использовать CGI, добавив следующую строку
#handleCGI(service)
# Необходимо предварительно импортировать handleCGI из jsonrpc

Код в листинге 15 может использовать либо MySQL, который легко установить, либо sqlite3, который поставляется вместе с Python. Для использования sqlite3 задайте переменной use_mysql значение False.

В листинге 16 показан unit-тест, проверяющий этот сервис. Этот тест сыграл важную роль в разработке данного примера приложения. В листинге показан служебный класс, используемый unit-тестом.

Листинг 16. TestContactService
import unittest
from contacts import ContactService
from dbscript import *

class TestContactService(unittest.TestCase):

    def setUp(self):
        self.cs = ContactService()
        try:
            drop_table()
        except:
            print "unable to drop contact table"
        try:
            create_table()        
        except:
            print "unable to create contact table"

    def testAdd(self):
        clear_table()
        cs = self.cs
        cs.addContact({"name":"Richard",
                       "phone":"5205551212",
                       "email":"rick@rick.com"
                      })
        list = cs.listContacts()
        print list
        found = False
        for cdict in list:
            if cdict["name"]=="Richard": found = True
        self.assertTrue(found)

    def testUpdate(self):
        cs = self.cs
        insert_test_data()
        cs.updateContact(
            {"name":"Richard",
                       "phone":"5205551212",
                       "email":"rick@rick.com",
                       "id":1})

        list = cs.listContacts()
        print list
        found = 0
        for cdict in list:
            if cdict["name"]=="Richard": found +=1
        self.assertTrue(found==1)


    def testRemove(self):
        cs = self.cs
        insert_test_data()
        cs.removeContact(
            {"name":"Richard",
                       "phone":"5205551212",
                       "email":"rick@rick.com",
                       "id":1})

        list = cs.listContacts()
        print list
        found = 0
        for cdict in list:
            if cdict["name"]=="Richard": found +=1
        self.assertTrue(found==0)


        
        
if __name__ == '__main__':
    unittest.main()

Сценарий dbscript.py, показанный в листинге 17, может создать таблицу контактов либо в MySQLdb, либо в sqlite3.

Листинг 17. Сценарий dbscript, используемый для создания, удаления и заполнения таблицы контактов
use_mysql = True

if use_mysql:
    import MySQLdb as db_api
else:
    import sqlite3 as db_api

db_url = "/tmp/contacts"


create_table_sql = """ 
create table contact (
     id INTEGER %s PRIMARY KEY, 
     name VARCHAR(50), 
     phone VARCHAR(50), 
     email VARCHAR(50));
"""

if use_mysql:
    create_table_sql = create_table_sql % ("AUTO_INCREMENT",)
else:
    create_table_sql = create_table_sql % ("",)


def run_script(func):
    if use_mysql:
        connection =  db_api.connect(passwd="mypassword", db="contactdb", user="root")
    else:
        connection =  db_api.connect(db_url)

    cursor = connection.cursor()
    try:
        func(cursor)
        connection.commit()
        cursor.close()
    finally:
        connection.close()
    
def create_table():
    def ct(cursor):
        cursor.execute(create_table_sql)

    run_script(ct)

def drop_table():
    def dt(cursor):
        cursor.execute("drop table contact;")
    run_script(dt)

def clear_table():
    def dt(cursor):
        cursor.execute("delete from contact;")
    run_script(dt)

def insert_test_data():
    def itd(cursor):
        if use_mysql:
            cursor.execute("insert into contact (id, name, phone, email) values (NULL, 
               'Bob', '5', 'b@b.com');") 
            cursor.execute("insert into contact (id, name, phone, email) values (NULL, 
                'Rick', '5', 'b@b.com');")
            cursor.execute("insert into contact (id, name, phone, email) values (NULL, 
                'Sam', '5', 'b@b.com');")
        else:
            cursor.executescript(""" 
    insert into contact (id, name, phone, email) values (NULL, "Bob", "5", "b@b.com"); 
    insert into contact (id, name, phone, email) values (NULL, "Rick", "5", "b@b.com"); 
    insert into contact (id, name, phone, email) values (NULL, "Sam", "5", "b@b.com"); 
           """)

    run_script(itd)

Сценарий dbscript может создавать и удалять таблицу контактов, а также заполнять ее тестовыми данными, используемыми в unit-тестах. После завершения работы над сервисом JSON-RPC можно настроить Apache HTTPD на работу с этим сервисом. Для этого следует добавить в файл httpd.conf код, показанный в листинге 18.

Листинг 18. /etc/apache2/httpd.conf
Alias /services "/home/rick/services" 

<Location /services/>
    AddHandler mod_python .py
    PythonHandler jsonrpc
</Location>

Помните, что после внесения изменений в сервис, его необходимо перезапускать, как показано в листинге 19.

Листинг 19. Перезапускаем Apache2 для вступления в силу изменений в mod_python
$sudo /etc/init.d/apache2 restart

При запуске прокси-объектов JSON-RPC в Pyjamas вы можете столкнуться с досадными повторяющимися ошибками. Для отладки этих ошибок я использовал отдельную клиентскую библиотеку JSON-RPC, как показано в листинге 20.

Листинг 20. JSON-RPC клиент для Python
from jsonrpc import ServiceProxy, JSONRPCException
 
cs = ServiceProxy("http://localhost/services/contacts.py")
 
if cs.test()=="test":
    print "connected"
 
try:
    cs.addContact(
        {"name":"Larry Wall", 
         "phone":"5551212", 
         "email":"rick@rick.com"})
 
except Exception, e:
    print e.error
    print `e.error`

Предыдущий шаг весьма важен в тестировании и отладки программ Pyjamas. Разработка с помощью Pyjamas пока находится в стадии зарождения, поэтому хорошо иметь еще один способ тестирования сервисов JSON-RPC.

В данном примере мы просто переключили сервис ContactService на использование JSONProxy. JSONProxy - это встроенный в Pyjamas клиент для работы с JSON-RPC. Можно создать прокси-объект для написанного нами сервиса, как показано на примере класса ContactsJSONProxy в листинге 21. Как известно, сервис JSON асинхронно возвращает объекты с результатами работы сервиса. Поэтому при вызовах методов прокси-объекта JSON мы передаем экземпляр класса ContactService, в котором реализован метод onRemoteResponse для асинхронного получения ответов от сервиса.

Листинг 21. Использование JSONRPC в приложении для работы с контактами
from pyjamas.JSONService import JSONProxy
...
class Contact:
    def __init__(self, name="", email="", phone="", id=None):
        self.name = name
        self.email = email
        self.phone = phone
        self.id = id
    def to_dict(self):
        return {"name":self.name, "email":self.email, 
                "phone":self.phone, "id":self.id}


class ContactsJSONProxy(JSONProxy):
    def __init__(self):
        JSONProxy.__init__(self, "/services/contacts.py", 
                           ["addContact", "removeContact", 
                            "updateContact", "listContacts","test"])

    

class ContactService:
    def __init__(self, callback):
        self.callback = callback
        self.proxy = ContactsJSONProxy()

    def test(self):
        self.proxy.test(self)

    def addContact(self, contact):
        self.callback.showStatus("Add contact called")
        self.proxy.addContact(contact.to_dict(), self)

    def updateContact(self, contact):
        self.callback.showStatus("Update contact was called")
        self.proxy.updateContact(contact.to_dict(), self)


    def removeContact(self, contact):
        self.callback.showStatus("Remove contact was called")
        self.proxy.removeContact(contact.to_dict(), self)

        
    def listContacts(self):
        self.proxy.listContacts(self)


    def onRemoteResponse(self, response, request_info):        
        if request_info.method == "addContact":
            self.callback.service_eventAddContactSuccessful()
        elif request_info.method == "updateContact":
            self.callback.service_eventUpdateContactSuccessful()
        elif request_info.method == "listContacts":
            def toContact(x):
                return Contact(x["name"], x["email"], x["phone"], x["id"])  
            contacts = map(toContact, response)
            self.callback.service_eventListRetrievedFromService(contacts)
        elif request_info.method == "removeContact":
            self.callback.service_eventRemoveContactSuccessful()
        else:
            self.callback.showStatus(""" REQ METHOD = %s RESP %s """ %
                 (request_info.method,response)) 

    def onRemoteError(self, code, errobj, request_info):
        message = errobj['message']
        if code != 0:
            self.callback.showStatus("HTTP error %d: %s" % (code, message))
        else:
            json_code = errobj['code']
            self.callback.showStatus("JSONRPC Error %s: %s" % (json_code, message))

Остальной код клиента похож на предыдущую версию, были сделаны лишь некоторые косметические изменения. Удивительно и ценно то, что клиент, использующий настоящий удаленный сервис RPC, немногим отличается от версии клиента, использующего локальный сервис-заглушку. Это позволяет быстро разрабатывать графический интерфейс, а затем просто подключать сервис JSON-RPC, разработка и отладка которого ведется отдельно.

Заключение

В этой первой части цикла статей "Введение в Pyjamas" мы познакомились с историей создания и идеологией, стоящей за Pyjamas. Также мы рассмотрели на примере создание Pyjamas-приложения с помощью Pyjamas, mod_python и JSON-RPC для Python. В второй статье цикла мы расскажем, как создавать пользовательские компоненты Pyjamas.

Благодарности

Выражаю особую благодарность Люку Кеннету Кессону Лейтону (Luke Kenneth Casson Leighton) за рецензирование это статьи и чрезвычайно важные комментарии. Он также помог довести примеры до рабочего состояния и дал несколько советов по отладке приложений.


Ресурсы для скачивания


Похожие темы


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Web-архитектура
ArticleID=855431
ArticleTitle=Введение в Pyjamas: Часть 1. Используем объединенную мощь GWT и Python
publish-date=01182013