• Home
  • Учебник по ExtJS
  • О сайте
  •  


    Новый SDK версии 1.1.3

    Сентябрь 17th, 2008

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

    Новые возможности этого SDK:

    • Поддержка модулей zipimport и zipserve, позволяющих работать с сжатыми архивами (последний имеет возможность отдавать серверу статические файлы из zip-архива). В связи с этим теперь стало возможным преодолеть ограничение на 1000 файлов приложения.
    • В состав консоли управления включен просмотрщик содержимого кэша memcache (доступ к нему можно получить по адресу http://localhost:8080/_ah/admin при запуске приложения в локальной среде).
    • Интерфейс URLFetch теперь позволяет отключать автоматический редирект по новому адресу загружаемого документа.
    • Доступы составные индексы с повторяющимися свойствами.

    Также были исправлены следующие баги:

  • Реализована корректная работа с разделителями параметра static_dir path под windows.
  • Работает отправка электронной почты с содержимым типа unicode.
  • Исправлена работа функции db.run_in_transaction, которая ранее не позволяла создавать два и более новых корневых объекта в одной транзакции.
  • Новый SDK доступен для загрузки, и мы, как обычно, ждем от вас сообщений в группе платформы!




    Ограничение доступа по списку пользователей

    Сентябрь 6th, 2008

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

    Модель AuthorizedUser используется для хранения списка авторизованных пользователей.

    class AuthorizedUser(db.Model):     user = db.UserProperty()

    Все классы обработчиков запросов, которые требуют авторизацию пользователей, должны быть наследниками от класса AuthorizedRequestHandler, а методы get и put в самом начале должны выполнять проверку возвращаемого значения методом authorize:

    Read the rest of this entry »




    Некоторые обновления Datastore API

    Август 25th, 2008

    Ранее команда разработки App Engine произвела обновление некоторых компонентов системы, а теперь дошла очередь до улучшений в работе хранилища.

    Сначала следует упомянуть пакетную запись. Теперь вы можете включить объекты из нескольких различных групп в одну операцию db.put() или db.delete(). Изменения объектов будут являться атомарными операциями только в рамках отдельной группы объектов, но использование одного обращения к хранилищу вместо нескольких отдельных последовательных вызовов, которые требовались ранее, более эффективно.

    Также ожидается, что в следующем релизе платформы, будут поддерживаться индексы с повторяющимися свойствами. К примеру:

    Kind: Post
     properties:
     - name: tag
     - name: tag

    Такие индексы будут полезны в некоторых случаях; смотрите раздел Запросы и индексы для дополнительной информации.

    Ну и как обычно, обновите свой SDK и просмотрите включенный в него полный список изменений в системе.




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

    Август 20th, 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)



    Полнотекстовый поиск в Google App Engine

    Август 19th, 2008

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

    Когда платформа Google App Engine была впервые представлена широким массам, многие недоумевали, почему в Datastore API не реализован полнотекстовый поиск. Очень странно – бизнес Google основан по поиске, однако эти новое передовое решение его не поддерживает!

    На сегодняшний момент интерфейс Google App Engine имеет примитивную реализацию полнотекстового поиска данных в хранилище, скрытую в недрах модуля google.appengine.ext.search. (По которому практически нет никакого описания, кроме самих исходных кодов). Вы можете использовать этот модуль, создавая модели наследники от класса search.SearchableModel, вместо обычного db.Model.

    К примеру так:

    from google.appengine.ext import db
    from google.appengine.ext import search
    
    class Article(search.SearchableModel):
      title = db.TextProperty()
      publishDate = db.DateTimeProperty(auto_now_add=True)
      text = db.TextProperty()
    
    # НЕ индексируется
    article = Article(text ='''This is the totally secret article text
    which talks about sausages and cheese in the middle of itself.''')
    article.title = "Fine cuisine"
    article.save()
    
    # индексируется
    article = Article()
    article.title = "What I feed my dogs"
    article.text = '''This is the totally secret article text
    which talks about sausages and cheese in the middle of itself.'''
    article.save()
    
    print "Результаты"
    
    query = Article.all().search("sausages cheese dogs").order("-publishDate")
    for a in query:
      print "%s | %s" % (a.title, a.publishDate)
    
    print "Конец вывода"

    Ограничения

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

    Латентность

    Философия работы хранилища заключается в том, что на сегодняшний момент дисковое пространство очень дешево, и соответственно выполнять обработку данных следует при сохранении объекта. Таким же образом работают модели SearchableModel, при вызове метода save() они выполняют построение индексов. Это означает, что модели типа SearchableModels будут работать всегда медленее стандартных моделей. Имейте всегда это ввиду.

    Индекс индекса

    Как вы знаете, пакет разработки Google App Engine SDK создает описание для всех необходимых индексов в файле index.yaml, для которых возникает потребность при разработке приложения. Однако, если вы не имеете возможности провести тесты приложения, покрывающие целиком все его запросы, тогда необходимо добавить описания соответствующих индексов в файл index.yaml вручную. В этих случаях необходимо учесть, что полнотекстовый индекс содержится в свойстве с именем __searchable_text_index. К примеру, индекс можно определить как:

      - kind: Article
        properties:
        - name: __searchable_text_index
        - name: publishDate
          direction: desc
    

    Это все о работе полнотекстового поиска в App Engine. Пока он не поражает обилием своих возможностей, однако этого хватит для большинства применений.




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

    Август 14th, 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())

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




    Извлечение случайных объектов из хранилища

    Август 13th, 2008

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

    # Определение модели class myModel(BaseModel):     random = db.FloatProperty(required=True)  # Создание объекта myModelInstance = myModel(random = random.random()) myModelInstance.save()  # Выполнение запроса к хранилищу rand = random.random() gql = "SELECT * FROM myModel WHERE random >= :random ORDER BY random ASC LIMIT 1" randomSantaRelation = GqlQuery(gql, random=rand).get() if randomSantaRelation is None:     gql = "SELECT * FROM myModel WHERE random <= :random ORDER BY random DESC LIMIT 1"     randomSantaRelation = GqlQuery(gql, random=rand).get()

    Что мы делаем? Мы присваиваем при создании нового объекта его свойству случайное число с плавающей точкой, которое будет выглядеть следующим образом:

    0.216565955485

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

    0.628912291991
    0.416565323566
    0.216565955485
    0.118278328322
    0.013212121212

    Если случайное значение, заданное в параметрах запроса GqlQuery будет 0.898912291991, то результат будет пустой. В этом случае и сработает резервный запрос.