KeyError во время очереди push-задач python GAE с помощью cron

Я очень близок к завершению проекта, который использует очереди push-задач в GAE для отправки дополнительных электронных писем пользователям. Однако я продолжаю получать KeyError и не знаю, почему. Я искал хорошие модели для своего проекта, но не нашел достойных примеров, использующих несколько параметров. Документация GAE улучшилась за последний месяц, но все еще оставляет желать лучшего.

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

app.yaml:

application: gae-project
version: 1
runtime: python27
api_version: 1
threadsafe: true

handlers:
- url: /cron/sendfu
  script: main.app
  login: admin

- url: /emailworker
  script: main.app
  login: admin

- url: /worker
  script: main.app
  login: admin

- url: /.*
  script: main.app
  login: required

cron.yaml:

cron:
- description: sends follow-up emails
  url: /cron/sendfu
  schedule: every day 20:00

queue.yaml:

total_storage_limit: 120M
queue:
- name: emailworker
  rate: 1/s
  bucket_size: 50
  retry_parameters:
    task_retry_limit: 5
    task_age_limit: 6h
    min_backoff_seconds: 10
    max_backoff_seconds: 60

main.py:

import webapp2
import datetime
from google.appengine.ext import db
from google.appengine.api import users
from google.appengine.api import taskqueue
import jinja2
import os

jinja_environment = jinja2.Environment(
loader=jinja2.FileSystemLoader(os.path.dirname(__file__)))

class emailJobs(db.Model):
    """ Models an a list of email jobs for each user """
    triggerid = db.StringProperty()  #Trig id
    recipientid_po = db.StringProperty() # id
    recipientlang = db.StringProperty()  #Language
    fu_email_sent = db.DateTimeProperty() 
    fuperiod = db.IntegerProperty() # (0 - 13)
    fu1 = db.DateTimeProperty() 
    fu2 = db.DateTimeProperty()
    
    @classmethod
    def update_fusent(cls, key_name, senddate):
        """ Class method that updates fu messages sent in the GAE Datastore """
        emailsjobs = cls.get_by_key_name(key_name)
        if emailsjobs is None:
            emailsjobs = cls(key_name=key_name)
        emailsjobs.fu_email_sent = senddate
        emailsjobs.put()

def timeStampFM(now):
    d = now.date()
    year = d.year
    month = d.month
    day = d.day
    t = now.time()
    hour = t.hour
    minute = t.minute + 5
    second = t.second
    today_datetime = datetime.datetime(year, month, day, hour, minute, second)
    return today_datetime


class MainPage(webapp2.RequestHandler):
    """ Main admin login page """
    def get(self):
        if users.get_current_user():
            url = users.create_logout_url(self.request.uri)
            url_linktext = 'Logout'
            urla = '/'
            url_admin = ""
            if users.is_current_user_admin():
                url = users.create_logout_url(self.request.uri)
                urla = "_ah/admin/"
                url_admin = 'Go to admin pages'
                url_linktext = 'Logout'
             
        else:
            url = users.create_login_url(self.request.uri)
            url_linktext = 'Login'

        template_values = {
            'url': url,
            'url_linktext': url_linktext,
            'url_admin': url_admin,
            'urla': urla,
            }

        template = jinja_environment.get_template('index.html')
        self.response.out.write(template.render(template_values))


class sendFollowUp(webapp2.RequestHandler):
    """ Queries Datastore for fu dates that match today's date, then adds them to a task queue """
    def get(self):
    
        now = datetime.datetime.now()
        now_dt = now.date() #today's date to compare with fu dates
   
        q = emailJobs.all()
        q.filter('fuperiod >', 0)
        q.filter('fuperiod <', 99)

        for part in q:
            guid = str(part.recipientid_po)
            lang = str(part.recipientlang)
            trigid = str(part.triggerid)

            if part.fuperiod == 1:
                fu1rawdt = part.fu1
                fu1dt = fu1rawdt.date()
                if fu1dt == now_dt:
                    follow_up = '1'
                
            if part.fuperiod == 2:
                fu2rawdt = part.fu2
                fu2dt = fu2rawdt.date()
                if fu2dt == now_dt:
                    follow_up = '2'
                
            if follow_up != None:
                taskqueue.add(queue_name='emailworker', url='/emailworker', params={'guid': guid,
                                                                                'fu': follow_up,
                                                                                'lang': lang,
                                                                                'trigid': trigid,
                                                                                })
        self.redirect('/emailworker')


class pushQueue(webapp2.RequestHandler):
    """ Sends fu emails, updates the Datastore with datetime sent """

    def store_emails(self, trigid, senddate):
        db.run_in_transaction(emailJobs.update_fusent, trigid, senddate)
        
    def get(self):
        fu_messages = {'1': 'MS_x01', 
                       '2': 'MS_x02',
                       }
        langs = {'EN': 'English subject',
                 'ES': 'Spanish subject',
                 }
    
        fu = str(self.request.get('fu'))
        messageid = fu_messages[fu]

        lang = str(self.request.get('lang'))
        subject = langs[lang]
    
        now = datetime.datetime.now()
        senddate = timeStampFM(now)
     
        guid = str(self.request.get('guid'))
        trigid = str(self.request.get('trigid'))
    
        data = {}
        data['Subject'] = subject
        data['MessageID'] = messageid
        data['SendDate'] = senddate
        data['RecipientID'] = guid
        # Here I do something with data = {}
    
        self.store_emails(trigid, senddate)
    
app = webapp2.WSGIApplication([('/', MainPage),
                           ('/cron/sendfu', sendFollowUp),
                           ('/emailworker', pushQueue)],
                           debug=True)

Когда я тестирую задание cron по адресу: localhost:8086/cron/sendfu

Он перенаправляет на: localhost: 8086/emailworker

и я получаю следующее сообщение об ошибке:

Внутренняя ошибка сервера

Сервер либо ошибся, либо не может выполнить запрошенную операцию.

Traceback (most recent call last):
  File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/lib/webapp2-2.5.2/webapp2.py", line 1535, in __call__
rv = self.handle_exception(request, response, e)
  File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/lib/webapp2-2.5.2/webapp2.py", line 1529, in __call__
rv = self.router.dispatch(request, response)
  File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/lib/webapp2-2.5.2/webapp2.py", line 1278, in default_dispatcher
return route.handler_adapter(request, response)
  File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/lib/webapp2-2.5.2/webapp2.py", line 1102, in __call__
return handler.dispatch()
  File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/lib/webapp2-2.5.2/webapp2.py", line 572, in dispatch
return self.handle_exception(e, self.app.debug)
  File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/lib/webapp2-2.5.2/webapp2.py", line 570, in dispatch
return method(*args, **kwargs)
  File "/Users/me/Documents/workspace/gae-project/src/main.py", line 478, in get
messageid = fu_messages[fu]
KeyError: ''

из журналов:

INFO     2013-03-05 03:03:22,337 dev_appserver.py:3104] "GET /cron/sendfu HTTP/1.1" 302 -
ERROR    2013-03-05 03:03:22,348 webapp2.py:1552] ''
Traceback (most recent call last):
  File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/lib/webapp2-2.5.2/webapp2.py", line 1535, in __call__
rv = self.handle_exception(request, response, e)
  File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/lib/webapp2-2.5.2/webapp2.py", line 1529, in __call__
rv = self.router.dispatch(request, response)
  File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/lib/webapp2-2.5.2/webapp2.py", line 1278, in default_dispatcher
return route.handler_adapter(request, response)
  File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/lib/webapp2-2.5.2/webapp2.py", line 1102, in __call__
return handler.dispatch()
  File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/lib/webapp2-2.5.2/webapp2.py", line 572, in dispatch
return self.handle_exception(e, self.app.debug)
  File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/lib/webapp2-2.5.2/webapp2.py", line 570, in dispatch
return method(*args, **kwargs)
  File "/Users/me/Documents/workspace/gae-project/src/main.py", line 478, in get
messageid = fu_messages[fu]
KeyError: ''
INFO     2013-03-05 03:03:22,355 dev_appserver.py:3104] "GET /emailworker HTTP/1.1" 500 -
INFO     2013-03-05 03:03:22,509 dev_appserver.py:3104] "GET /favicon.ico HTTP/1.1" 404 -

строки:

469    def get(self):
470        fu_messages = {'1': 'MS_x01', 
471                       '2': 'MS_x02',
472                       }
473        langs = {'EN': 'English subject',
474                 'ES': 'Spanish subject',
475                 }
476
477        fu = str(self.request.get('fu'))
478        messageid = fu_messages[fu]

person 655321    schedule 05.03.2013    source источник
comment
Можете ли вы попытаться указать это на пригодную для использования трассировку стека, например, где строка 478 встречается в main.py? Кроме того, какой запрос вызывает это? Просто запустить сервер?   -  person bossylobster    schedule 05.03.2013
comment
Привет, бл, извините, я новичок в питоне и GAE и не понимаю всего жаргона. Единственное, что есть в строке 478: messageid = fu_messages[fu]. Запрос — это задание cron, которое я запускаю на сервере разработки. Я надеюсь, что это отвечает на ваш вопрос. Мой код выглядит нормально?   -  person 655321    schedule 05.03.2013
comment
Можете ли вы включить журналы с сервера разработки для неудачного запроса? Можете ли вы включить код вокруг строки 478?   -  person bossylobster    schedule 05.03.2013
comment
Привет бл, я надеюсь, что это то, что вы просили. Часть кода, которую я обрезал, находится после data['RecipientID'] = guid, но проблема возникает намного раньше.   -  person 655321    schedule 05.03.2013


Ответы (1)


Когда вы звоните

fu = str(self.request.get('fu'))

если в запросе нет 'fu', self.request.get вернет пустую строку (''). Поэтому, когда вы пытаетесь

messageid = fu_messages[fu]

он ищет пустую строку в

fu_messages = {'1': 'MS_x01', 
               '2': 'MS_x02',
               }

который имеет только '1' и '2' в качестве ключей.

Причина, по которой ваш обработчик pushQueue не видит params, который вы отправляете через

params = {
    'guid': guid,
    'fu': follow_up,
    'lang': lang,
    'trigid': trigid,
}
taskqueue.add(queue_name='emailworker', url='/emailworker', 
              params=params)

потому что вы используете обработчик GET вместо обработчика POST или PUT. Как указано в документации:

Параметры кодируются как application/x-www-form-urlencoded и устанавливаются для полезной нагрузки.

Таким образом, полезная нагрузка запроса содержит ваш параметр 'fu', но, поскольку это запрос GET, полезная нагрузка отбрасывается (именно так работает HTTP, а не конкретно для App Engine). Если вы используете POST в качестве обработчика, полезная нагрузка будет доставлена, как и ожидалось.

Я заметил, что ваш код очень похож на задокументированный образец. , но просто использует get, где в образце используется post.

person bossylobster    schedule 05.03.2013
comment
У меня есть только полные данные в Datastore для поля fu. Все сущности имеют значения. Я попробовал запрос в интерактивной консоли, и он вернул значения. Что может быть не так? - person 655321; 05.03.2013
comment
Когда вы используете self.request.get, 'fu' поступает из запроса, а не из хранилища данных. Вам следует освежить доступ к данным из хранилища данных (developers.google.com/appengine /docs/python/ndb/overview) и пересмотрите свой код после этого. - person bossylobster; 05.03.2013
comment
Сценарий начинается с запроса к хранилищу данных, но затем он передает его в очередь задач, и здесь начинается проблема. Это taskqueue.add(queue_name='emailworker'... и получение тех параметров, которые я не получаю. Документация по очередям задач сбивает с толку. - person 655321; 05.03.2013
comment
Как вы понимаете, что сценарий начинается с запроса к хранилищу данных? Почему вы используете очередь задач для запроса к хранилищу данных? Неудачный запрос исходит от cron и направляется вашему обработчику. Запрос к обработчику — это не то же самое, что запрос к хранилищу данных. Какая часть документации вас смущает? - person bossylobster; 05.03.2013
comment
Извините, я не обучен программированию, поэтому жаргон не моя сильная сторона. Я запрашиваю хранилище данных с помощью задания cron, потому что хранилище данных обновляется другим процессом, поэтому я просматриваю новые значения по расписанию. Затем я передаю эти новые значения через очередь задач, которую я в конечном итоге использую в вызове REST API к другой службе для отправки электронной почты. Часть, которую я не понимаю, это то, что происходит с очередью задач, обработчиком. Чтобы ответить на ваш вопрос, документация очереди задач сбивает с толку. У него нет полных или нескольких примеров. Думаю, как новичок, я ожидаю слишком многого. - person 655321; 05.03.2013
comment
Я обновил свой ответ, прочитав больше вашего кода. Никто не хочет читать так много кода, и только после нашей беседы в комментариях я смог получить представление о том, что искать, а не делать полный обзор кода вашего приложения. Ключом к ответам на вопросы здесь является сужение вашей проблемы. Это легко могло быть двумя вопросами SO: 1) Почему self.request.get('foo') пустая строка? и 2) Почему полезная нагрузка не отправляется, когда я использую params= с taskqueue.add? - person bossylobster; 05.03.2013
comment
Спасибо! Я изменил запрос на отправку, но получил сообщение об ошибке, в котором говорилось, что это не разрешено. Итак, мне нужно иметь как запрос на получение, так и запрос на публикацию в обработчике sendFollowUp? Один, чтобы сначала получить значения из запроса к хранилищу данных, а затем один, чтобы отправить их в очередь задач? Если да, то как перейти от получения к публикации в том же обработчике (sendFollowUp)? Спасибо еще раз. - person 655321; 06.03.2013
comment
Пожалуйста, начните новый вопрос, и мы можем решить эту следующую проблему там. - person bossylobster; 06.03.2013