Как скачать свой код с App Engine?

Сентябрь 4, 2008

На форумах часто появляются сообщения разработчиков, которые создали проект, опубликовали его на App Engine, а потом вдруг потеряли его исходники (всякое бывает). Несмотря на то, что платформа имеет очень продвинутую панель управления приложениями и их версиями, к сожалению, возможности скачать уже опубликованный код стандартными средствами нет.

Конечно же, существует отдельный вариант загрузить вместе с проектом специальный AJAX shell, который позволит просматривать код опубликованного приложения. Скачать подобное решение можно отсюда.

Давайте рассмотрим пример просмотра кода самой AJAX-shell, которая расположена по адресу http://shell.appspot.com.

  1. Сначала узнаем, где мы находимся:
    >>> os.getcwd() 'base/data/home/apps/shell/1.29/'
  2. Выведем содержимое каталога:
    >>> os.listdir(".") ['shell.py', 'templates']
  3. Далее откроем интересующий нас файл с помощью вывода его содержимого на экран:
    >>> file = open("shell.py") >>> for line in file:     print line #!/usr/bin/python [далее содержимое файла]

Возможно в будущем появятся какие-то комплексные удобные решения, которые позволят интерактивно взаимодействовать с кодом приложения. Чем не идея для нового приложения?


XML сервисы, часть вторая

Август 28, 2008

Недавно мы рассматривали создание приложения на платформе App Engine, которое позволяет IP телефонам Cisco получать информацию о прогнозе погоды, новостях и курсе валют.

К сожалению, пользователи, которые же в первые дни появились у этого сервиса, обнаружили в нем два существенных недостатка:

  • Мы отображаем только погоду только по Москве, хотя сервис у нас глобальный
  • Существуют проблемы на разных версиях прошивок для телефонов, которые ожидают данные на русском языке в кодировке cp-1251.

Первый недочет решено было исправить, добавив опциональный параметр city к URL сервиса, равный коду города на сайте ГисМетео, на котором мы берем данные погоды. Таким образом строка для телефонов Питера выглядит как http://xmlphones.appspot.com/?city=26063. Второй аналогично будет называться encode и содержать при необходимости задание кодировки windows-1251.

Прочтите эту запись до конца »


Подсказки по использованию App Engine

Август 20, 2008

Как из приложения получить его идентификатор и версию?

Используйте функцию os.getcwd() или переменную окружения os.environ['PATH_TRANSLATED']

>>> os.getcwd()
'/base/data/home/apps/shell/1.21'
>>> os.getcwd().split('/')[-2]
'shell'
>>> os.getcwd().split('/')[-1]
'1.21'

>>> os.environ['PATH_TRANSLATED']
'/base/data/home/apps/shell/1.21/shell.py'
>>> os.environ['PATH_TRANSLATED'].split('/')[-3]
'shell'

Как определить текущий хост?

Есть один очень интересный файл, который уникален на каждом сервере:

>>> open('/base/python_dist/search.config').read()
'datapath .\nsorttempdir .\ndisk /export/hdc3/borgletdata/dirs/prod-appengine.\
mpm_python_dist_v12.apphosting.77627982/bigfiledata/466024'

>>> open('/base/python_dist/search.config').read()
'datapath .\nsorttempdir .\ndisk /export/hdc3/borgletdata/dirs/prod-appengine.\
mpm_python_dist_v12.apphosting.77627739/bigfiledata/465336'

Вы можете идентифицировать машину, на которой работает данный процесс, проанализировав хэш содержимого из этого файла. К примеру так:

def get_server_id():
    try:
        fd   = open('/base/python_dist/search.config')
        data = fd.read()
        fd.close()
    except IOError:
        return 'unknown'

    return '%s' % data.__hash__()

Google не сообщает, на скольких серверах работает ваше приложение (и скорее всего это будет зависеть от генерируемого вашим сайтом трафика). Для определения того, сколько машин используется для функционирования приложения можно воспользоваться следующей методикой: включить server_id в содержимое страницы сайта. Затем достаточно провести несколько загрузок, чтобы понять сколько уникальных идентификаторов сервера будет выдано.

$ for i in `seq 20`; do
    curl -s http://cometchat.appspot.com|\
    grep server_id; \
  done    |sort -n|uniq -c

     20 server_id: '7341146770217830363'

В примере видно, что приложение работает только на одном сервере.

Как можно идентифицировать текущий процесс?

И следующий вопрос: сколько процессов моего приложения запущено на одном сервере? Можно проанализировать с помощью глобальной переменной:

the_process_global = "something"

def get_process_id():
    return '%s' % id(the_process_global)

Теперь мне известно, что приложение задействует два процесса:

$ for i in `seq 20`; do
    curl -s http://cometchat.appspot.com|\
    grep _id;
  done    |sort -n|uniq -c

    13 process_id: '12457625149327067176'
     7 process_id: '3996238433791648184'

Работаем ли мы в среде разработки или на сервере?

Я использую такой шаблон:

if os.environ.get('SERVER_SOFTWARE','').startswith('Devel'):
    HOST='local'
elif os.environ.get('SERVER_SOFTWARE','').startswith('Goog'):
    HOST='google'
else:
    # logging.error('Неизвестный сервер?')
    HOST='unknown'

Cookies?

Google в своем интерфейсе определило объекты request и response как наследники соответствующих классов из библиотеки WebOb. Таким образом мы можем из объекта request получить содержимое cookie запроса:

username = self.request.cookies.get('username', '')

К сожалению, вы не сможете напрямую использовать метод response.set_cookie из библиотеки WebOb. Но это можно всегда сделать вручную:

self.response.headers.add_header(
        'Set-Cookie',
        'username=%s; expires=Fri, 31-Dec-2020 23:59:59 GMT' \
          % username.encode())

Дополнительные методы работы с cookie есть в группе обсуждения на английском языке.

Отладка работы с данными хранилища

Я создал простой отладчик для работы с хранилищем. Он добавляет отладочную информацию в конец каждой создаваемой страницы. Для его использования требуется, чтобы классы обработчиков наследовались от debug.DebugMiddleware вместо webapp.RequestHandler.

К примеру:

class List(debug.DebugMiddleware):
    def get(self):
        ... blabla ...

Отдалочная информация будет выглядеть так:

**** Request took:   830ms/170ms (real time/cpu time)
**** GQLs, datastore accessed 1 times.
98ms GQL app: ":self"
            kind: "Image"
            Order {
            property: "modified"
            direction: 2
            }
            args: (50,) {}

GQL запрос, который сгенерировал ее:

ims = Image.all().order("-modified").fetch(50)

Другой пример отладочной информации:

**** Request took:   150ms/130ms (real time/cpu time)
**** GQLs, datastore accessed xx times.
  219ms PUT ({'full':...
  178ms PUT ({'full':...
    6ms GET ([datastore_types.Key.from_path('Image', 350L, _app=u'srv')],) {}
    2ms GET ([datastore_types.Key.from_path('Image', 349L, _app=u'srv')],) {}
    2ms GET ([datastore_types.Key.from_path('Image', 348L, _app=u'srv')],) {}

Этот отладчик очень удобно использовать в Django.

Динамическая загрузка изображений на сервер

Вот код, который я использую:

<form action="." method="post" enctype="multipart/form-data">
    <label>File: </label><input name="file" type="file"><br />
    <input type="submit">
</form>

На стороне сервера:

class Image(db.Model):
    name        = db.StringProperty()
    content     = db.BlobProperty()

class UploadImage(webapp.RequestHandler):
    def post(self):
        if 'file' not in self.request.POST:
            self.error(400)
            self.response.out.write("Файл не указан!")
            return

        if (self.request.POST.get('file', None) is None or
           not self.request.POST.get('file', None).filename):
            self.error(400)
            self.response.out.write("Файл не указан!")
            return

        file_data = self.request.POST.get('file').file.read()
        file_name = self.request.POST.get('file').filename

        im = Image()
        im.name    = file_name
        im.content = file_data
        im.save()
        self.response.out.write("Изображение %r сохранено." % im.name)

Как определить размер и тип изображения

Существует реализации функции getImageInfo, которая позволяет определять размер изображения без использования внешних библиотек. Использование очень простое:

content_type, width, height = getImageInfo(im.content)

Динамическая работа с изображениями

По этому поводу существует отдельная статья в официальной документации. Ниже приведен мой упрощенный код, который я использую:

class ServeImage(webapp.RequestHandler):
    def get(self, key):
        im = db.get(db.Key(key))
        if not im:
            self.error(404)
            return

        content_type, width, height = getImageInfo(im.content)
        self.response.headers.add_header("Expires", "Thu, 01 Dec 2014 16:00:00 GMT")
        self.response.headers["Content-Type"] = content_type
        self.response.out.write(im.content)

Web2py – первый взгляд

Август 18, 2008

Web2Py (ранее называвшийся Gluon) – это новое слово в фреймворках для Python. Он очень похож на Django, но позволяет создавать и разрабатывать приложения прямо в онлайне. Он делает за вас большую часть работы и теперь появилась возможность запустить его на платформе Google App Engine.

На сайте Vimeo выложен пример, показывающий начало разработки с Web2Py:


Загрузка файлов через форму Django Forms

Август 18, 2008

Поле FileField из библиотеки Django Forms можно привязать к свойству типа BlobProperty объекта хранилища. Ниже описано как это делается:

Модель:

from appengine_django.models import BaseModel
from google.appengine.ext import db

class AlbumItem(BaseModel):
  creationDate = db.DateTimeProperty(auto_now_add=True)
  image = db.BlobProperty(required=True)
  note = db.StringProperty()

Отображение формы:

class AlbumItemForm(ModelForm):
 image = FileField()
 note = CharField(widget=Textarea)

 class Meta:
  model = AlbumItem
  exclude = ("creationDate")

def albumentry(request):
 if request.POST:
  form = AlbumItemForm(request.POST, request.FILES)
  if form.is_valid():
   albumEntry = form.save()
   return HttpResponseRedirect('/showalbumentry/%s' % albumEntry.key())
  else:
   return render_to_response('albumentry.html', locals())
 else:
  form = AlbumItemForm()
  return render_to_response('albumentry.html', locals())

Содержимое указанного в коде файла шаблона albumentry.html тривиально, мы не будем полностью описывать его. Главное поместите в код формы шаблонную переменную {{form}} и не забудьте выставить атрибут enctype=»multipart/form-data» в теге <form>.

Все работает автоматически – содержимое файла с изображением, загружаемое на сервер, будет помещено в свойство image. Используемый код работает на версиях App Engine SDK 1.1, Django Helper r30 и Django 0.97.


Использование reCAPTCHA в Google App Engine

Август 17, 2008

Борьба со спамом приобретает промышленные масштабы, и одним из способов помешать искусственному интеллекту присылать нашему приложению «рекламные подарки», является установка на сайт CAPTCHA.

Одним из популярных разновидностей такого сервиса является reCAPTCHA, созданная в университете Карнеги Меллон, которая не только дает наилучшие результаты из себе подобных, но и помогает человечеству оцифровывать книги.

С чего начать?

Сначала необходимо получить ключ сервиса на сайте recaptcha.net.
После регистрации вы сможете создать неограниченное количество сервисных ключей для вашего домена.
Примечание: Если вы хотите, чтобы reCAPTCHA корректно работала в отладочной среде, то необходимо также получить ключ для localhost.
После генерации пары ключей, вы увидите следующую страницу:

Сохраните этот публичный и приватный ключ – они понадобятся вам позже. Если вдруг они будут случайно потеряны, то сможете вернуться на сайт recaptcha.net и снова их получить. Ни в коем случае не сообщайте свой приватный ключ.

После регистрации в сервисе, вам потребуется простой класс на языке

Python, адаптированный для App Engine. Скачать его можно здесь. После загрузки переименуйте его в captcha.py и скопируйте в каталог приложения (я предпочитаю размещать его в подкаталоге recaptcha/client/ – не забудьте добавить к каждому подкаталогу пустой файл __init__.py).

Для демонстрации работы мы дополним стандартный пример из Руководства для начинающих Google App Engine.

Откройте файл helloworld.py и добавьте строчки импорта модулей captcha и environ:

from os import environ
from recaptcha.client import captcha

Затем перейдите к контроллеру MainPage и добавьте следующее:

chtml = captcha.displayhtml(
  public_key = "ВАШ-ПУБЛИЧНЫЙ-КЛЮЧ",
  use_ssl = False,
  error = None)

template_values = {
   ...
   'captchahtml': chtml
}

Соответственно замените выражение ВАШ-ПУБЛИЧНЫЙ-КЛЮЧ своим ключом, иначе получите ошибку:
«Invalid public key. Make sure you copy and pasted it correctly.»

Теперь перейдите к файлу с шаблоном и добавьте тэг captchahtml внутри формы:

<form>
  ...
    {{ captchahtml }}
</form>

После этого можно открыть свой браузер и увидеть reCAPTCHA в отдельном iframe:

Теперь после того, как получены отправленные через форму данные, нам необходимо выполнить проверку того, что в катпчу введен правильный код. Мы проведем изменение метода post класса Guestbook и добавим код:

def post(self):
  challenge = self.request.get('recaptcha_challenge_field')
  response  = self.request.get('recaptcha_response_field')
  remoteip  = environ['REMOTE_ADDR']

  cResponse = captcha.submit(
                 challenge,
                 response,
                 "ВАШ-ПРИВАТНЫЙ-КЛЮЧ",
                 remoteip)

    if cResponse.is_valid:
        # код введен верный
        # продолжаем работу приложения
    else:
      error = cResponse.error_code
    ...

Аналогично, замените выражение ВАШ-ПРИВАТНЫЙ-КЛЮЧ ранее полученным приватным ключом.

Информация, введенная пользователем в поле reCAPTCHA будет отправлена на проверку вместе с его IP адресом. Мы получим ответ от сервера reCAPTCHA в формате объекта RecaptchaResponse.

Объект RecaptchaResponse имеет два атрибута:

  • is_valid, установленный в True, если тест был пройден удачно (в противном случае False)
  • error_code , содержащий код ошибки API, если произошла какая-то проблема.

Примечание: В Руководстве для начинающих Google App Engine, информация введенная в формы, передавалась контроллеру Guestbook. В обычном случае мы будем передавать ее в контроллер MainPage, и обрабатывать код ошибки из объекта RecaptchaResponse (если он будет) методом displayhtml класса captcha:

chtml = captcha.displayhtml(
  public_key = "ВАШ-ПУБЛИЧНЫЙ-КЛЮЧ",
  use_ssl = False,
  error = cResponse.error_code)

Это позволит вывести читаемое сообщение об ошибке пользователю:

и дать ему возможность еще раз ввести ответ на CAPTCHA без необходимости повторного ввода других данных в поля формы.

Имейте ввиду: Для каждой отправленной пользователем CAPTCHA, выполняется удаленный запрос к серверу reCAPTCHA. Этот запрос является синхронным, и таким образом посетитель будет ждать некоторое время, пока сервер обработает этот запрос. В том случае, если сервер reCAPTCHA в этот момент не был доступен, будет возвращен код ответа recaptcha-not-reachable.


Операции AVG и SUM в Google App Engine

Август 14, 2008

Разработчики, которые ранее использовали обычные реляционные базы данных часто сразу не могут переложить свои навыки на хранилище App Engine и заваливают буквально шквалом вопросов. Один из них: Как выполнить операции SUM и AVG с данными хранилища?. Вы не можете напрямую выполнять в хранилище подобные запросы, вместо этого необходимо постоянно вести учет итогов и хранить их в специальном счетчике. Вот пример такой реализации:

Файл models.py:

from appengine_django.models import BaseModel
from google.appengine.ext import db
from google.appengine.ext.db import NotSavedError
from decimal import Decimal, getcontext

class Callable:
 # это класс выполняет обертывание метода, так что мы можем вызвать его напрямую без создания экземпляра.

 def __init__(self, anycallable):
  self.__call__ = anycallable

class GlobalCounter(BaseModel):
 name = db.StringProperty(required=True)
 value = db.IntegerProperty(required=True)

class Rating(BaseModel):
 user = db.UserProperty(required=True)
 rating = db.IntegerProperty(required=True) # скрываем его
 oldRating = 0

 def set_rating(self, val):
  self.oldRating = self.rating
  self.rating = val

 def put(self):
  self.save()

 def save(self):

  # получаем значения счетчиков из хранилища
  votes = GlobalCounter.all().filter("name = ", "numberOfVotes").get()
  total = GlobalCounter.all().filter("name = ", "totalAmountOfVotes").get()

  if (votes == None):
   votes = GlobalCounter(name = "numberOfVotes", value = 0)
   votes.save()

  if (total == None):
   total  = GlobalCounter(name = "totalAmountOfVotes", value = 0)
   votes.save()

  # проверяем, является ли рейтинг новым или пользователь просто меняет свой выбор
  isSaved = False
  try:
   self.key()
   isSaved = True
  except NotSavedError:
   isSaved = False

  if (isSaved):
   # если пользователь пересохраняет свой выбор (возможно меняя его) мы вычитаем из итогов старое значение и добавляем новое
   total.value = total.value - self.oldRating + self.rating
  else:
   votes.value += 1
   total.value += self.rating

  votes.save()
  total.save()

  BaseModel.save(self)

 def ___getAverage___():
  votesCounter = GlobalCounter.all().filter("name = ", "numberOfVotes").get()
  totalCounter = GlobalCounter.all().filter("name = ", "totalAmountOfVotes").get()
  getcontext().prec = 3
  return Decimal(str(totalCounter.value)) / Decimal(str(votesCounter.value))

 average = Callable(___getAverage___) # average() будет теперь вести себя как статическая переменная

 def ___getTotal___():
  totalCounter = GlobalCounter.all().filter("name = ", "totalAmountOfVotes").get()
  return totalCounter.value

 sum = Callable(___getTotal___) # sum() будет теперь вести себя как статическая переменная

А использовать это можно так… Файл tests.py:

import os
from decimal import Decimal, getcontext

#импортируем загрушки
from google.appengine.api import apiproxy_stub_map
from google.appengine.api import datastore_file_stub
from google.appengine.api import mail_stub
from google.appengine.api import urlfetch_stub
from google.appengine.api import user_service_stub

from google.appengine.api import users

# импортируем замену Django Forms из SDK App Enginee
from google.appengine.ext.db.djangoforms import ModelForm 

# Я использую библиотеку Python Mocker для создания тестовых объектов: http://labix.org/mocker
import mocker
from mocker import MockerTestCase

# импортируем нашу модель
from avgtest.models import Rating
from avgtest.models import GlobalCounter 

class TestStart(MockerTestCase):

 def setUp(self):

  apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()

  # Используем мнимое хранилище.
  # В этой точке приложения задается, что все обращения к хранилищу, например операции get и put,
  # будут выполняться с временными данными, расположенными в памяти.

  stub = datastore_file_stub.DatastoreFileStub(u'myTemporaryDataStorage', '/dev/null', '/dev/null')
  apiproxy_stub_map.apiproxy.RegisterStub('datastore_v3', stub)

  # Используем заглушку для UserService.
  apiproxy_stub_map.apiproxy.RegisterStub('user', user_service_stub.UserServiceStub())
  os.environ['AUTH_DOMAIN'] = 'gmail.com'
  os.environ['USER_EMAIL'] = 'myself@appengineguy.com' # set to '' for no logged in user
  os.environ['SERVER_NAME'] = 'fakeserver.com'
  os.environ['SERVER_PORT'] = '9999' 

  # Используем заглушку для urlfetch.
  apiproxy_stub_map.apiproxy.RegisterStub('urlfetch', urlfetch_stub.URLFetchServiceStub())

  # Используем заглушку для почты.
  apiproxy_stub_map.apiproxy.RegisterStub('mail', mail_stub.MailServiceStub()) 

  self.HttpResponseRedirect = self.mocker.replace("django.http.HttpResponseRedirect")

  self.render_to_response = self.mocker.replace("django.shortcuts.render_to_response")

 def testAverage(self):
  rA = Rating(user = users.get_current_user(), rating = 1)
  rA.save()
  self.assertEquals(1, Rating.average())
  rB = Rating(user = users.get_current_user(), rating = 4)
  rB.put()
  self.assertEquals(Decimal("2.5"), Rating.average())
  rC = Rating(user = users.get_current_user(), rating = 2)
  rC.save()
  self.assertEquals(Decimal("2.33"), Rating.average())

 def testSum(self):
  rA = Rating(user = users.get_current_user(), rating = 2)
  rA.save()
  rB = Rating(user = users.get_current_user(), rating = 4)
  rB.put()
  rC = Rating(user = users.get_current_user(), rating = 3)
  rC.save()
  self.assertEquals(9, Rating.sum())

 def testAverageChangeVote(self):
  rA = Rating(user = users.get_current_user(), rating = 1)
  rA.save()
  self.assertEquals(1, Rating.average())
  rB = Rating(user = users.get_current_user(), rating = 4)
  rB.put()
  self.assertEquals(Decimal("2.5"), Rating.average())
  rC = Rating(user = users.get_current_user(), rating = 2)
  rC.save()
  self.assertEquals(Decimal("2.33"), Rating.average())
  rC.set_rating(3) # пользователь C изменил свою оценку на 3
  rC.save()
  self.assertEquals(Decimal("2.67"), Rating.average())

Замечание по производительности: Приведенный выше пример показывает неэффективную работу с счетчиками, однако он указан только для объяснения принципов работы. На практике же необходимо использовать разделение счетчиков.


Правильная работа с Unicode в Python

Август 13, 2008
  1. Есть замечательная презентация в виде тезисов или видео. Усвоение этого материала снимет большинство типовых вопросов.
  2. Не используйте нигде в приложении однобайтовые кодировки (cp1251, koi8-r и подобные) – только UTF-8. App Engine всегда пытается задействовать по умолчанию UTF-8.
  3. Убедитесь, что ваш текстовый редактор сохраняет по умолчанию файл в UTF-8.
  4. В начало каждого файла с кодом добавляйте строчку # coding=UTF-8
  5. Поместите оператор u перед каждой строкой, которая содержит символы не из кодировки ascii. К примеру, на немецком u»Jag är en liten hatt och är bög»
  6. При обработке входящих запросов убедитесь что корректно используется request.encoding = «UTF-8″
  7. Используйте ugettext как псевдоним для _
  8. Если метод (такой как quote() или hashlib.sha224() требует указать в параметрах байтовую строку, приведите ее в формат unicode: theunicodestring.encode(«utf-8″)

Кроссдоменный прокси на Google App Engine

Август 12, 2008

Большинство браузеров не позволяют Javascript Ajax обращения к другим доменам, поэтому использование Flickr API с Google App Engine весьма затруднено. Описанный ниже код позволяет организовать простой прокси-сервер:

import cgi
import urllib
from google.appengine.ext import webapp
from google.appengine.api import urlfetch

class FlickrController(webapp.RequestHandler):
	"""Прокси для Ajax запросов к Flickr"""
	def get(self):
		flickrapiendpoint = 'http://api.flickr.com/services/rest/'
		flickrapikey = 'you_flicker_api_key'

		params = self.request.GET
		params.add('api_key', flickrapikey)
		params.add('format', 'json')
		apiquery = urllib.urlencode(params)

		result = urlfetch.fetch(url=flickrapiendpoint + '?' + apiquery, method=urlfetch.GET)
		self.response.out.write(result.content)

def main():
	application = webapp.WSGIApplication(
		[('/flickr/', FlickrController)],
		debug=True)
	wsgiref.handlers.CGIHandler().run(application)

if __name__ == "__main__":
	main()

Пользовательские фильтры Django в Google App Engine

Август 12, 2008

Хотите использовать собственные фильтры Django в App Engine без необходимости тащить вместе с приложением весь код Django целиком? Это делается легко несколькими строчками кода.

Сначала создаем специальный файл, который будет размещен в корне приложения. В моем случае я назвал его customfilters.py:

import re
from google.appengine.ext import webapp

register = webapp.template.create_template_register()

def escapeimg(body):
	return re.sub(r'&lt;img (.*)/&gt;', '[IMG]', body)

register.filter(escapeimg)

Затем добавим следующую строчку вне определения функции main() приложения, например, сразу после операторов импорта модулей:

"""Загрузка своих фильтров Django """
webapp.template.register_template_library('customfilters')

После этого вы можете использовать определенные в файле customfilters.py фильтры в любых шаблонах без необходимости задания % load foobar %