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

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

Рик Хайтауэр, разработчик, Mammatus Inc.

Рик Хайтауэр (Rick Hightower) - разработчик ПО с 20-летним опытом работы и постоянный автор материалов для developerWorks. Он работает техническим директором в Mammatus Inc. - компании из Сан-Франциско, занимающейся консультированием и обучением. Рик является соавтором популярной книги Java Tools for Extreme Programming, а также автором книги Programming the Java API with Python. Он входит в редакционную коллегию журнала Java Developer's Journal и регулярно публикует материалы о Java и Groovy на DZone.



18.01.2013

Введение

Инфраструктура 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-приложений.

Могучий XUL

В 2009 году Pyjamas-Desktop (инфраструктура, которая теперь является частью Pyjamas) также была портирована для работы с XUL. XUL - это расширение Firefox, предоставляющее примерно ту же функциональность, что WebKit для Safari. Pyjamas можно исполнять поверх XUL. Говорят, для переноса Pyjamas на XUL потребовалось всего два дня благодаря проекту Hulahop (команды OLPC Sugar) и разработчикам python-xpcom.

В недалеком прошлом возможность сделать приложение исключительно на основе технологии 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

Проблемы с привязками Python к XUL и WebKit

Привязка Python к MSHTML, по всей видимости, является лучшей из всех, а нижележащие привязки Python к WebKit и XUL постоянно меняются. Пока команда разработчиков WebKit не занимается привязкой Python к WebKit GTK, это причиняет использующим её разработчикам невыразимую боль и страдания.

Временами кажется, что привязки Python к WebKit и xulrunner намеренно разрушаются или как минимум игнорируются.

Помните, что Pyjamas-Desktop привязан не только к WebKit. Pyjamas позволяет Python разработчикам работать с WebKit, XUL и MSHTML. То есть Pyjamas-Desktop может использовать любой из этих трех механизмов браузера. Благодаря этому Pyjamas является как кросс-браузерным, так и кросс-платформенным набором виджетов GUI.

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.

Разделяй и властвуй

Я предпочитаю разрабатывать все GUI-приложения с использованием сервиса-заглушки, чтобы отделить разработку GUI от сохранения данных и от уровня бизнес-логики. Это позволяет сосредоточиться на логике GUI, не отвлекаясь на отладку удаленных RPC-вызовов и т.д.

Чтобы написать сервис-заглушку, необходимо понять, как приложение будет работать во время выполнения. Приложение будет делать асинхронные вызовы к сервису 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.
Листинг 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

Советы по использованию mod_python и sqllite3

Изначально мне казалось, что в этой статье у нас будет маленький сервис для маленького примера, и поэтому для него не нужны unit-тесты и журналирование. Это было большой ошибкой! Началось с того, что я попытался использовать sqllite3 (так как он поставляется вместе с Python) и столкнулся с проблемами блокировки файлов, которые вынудили меня переключиться на MySQL. Возьмите себе на заметку следующую информацию о mod_python, JSON-RPC и sqllite3:

  • sqlite3 странным образом блокирует файлы при локальном выполнении приложения с использованием unit-тестов, а также в дальнейшем при работе под apache с использованием su.
  • Довольно сложно отлаживать приложение, работающее с mod_python, так как в таком случае приложение не выдает сообщений об ошибках. Поэтому крайне важны журналирование и unit-тесты.

Я переключился на MySQL, сделал журналирование, надежную обработку исключений и написал unit-тесты. Вероятно, без unit-тестов и журналирования я бы так и не смог закончить этот пример. Возможно, вы сможете использовать и sqlite3, если всегда будете запускать unit-тесты с помощью sudo. Или же, возможно вы сможете найти другие способы обойти проблемы блокировки.

После завершения работы над логикой 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) за рецензирование это статьи и чрезвычайно важные комментарии. Он также помог довести примеры до рабочего состояния и дал несколько советов по отладке приложений.


Загрузка

ОписаниеИмяРазмер
Код приложения для управления контактамиpyjamas.zip2425 КБ

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


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