blob: 22d228c6149174ed1bbf2d7044fe3c9961a20c82 [file] [log] [blame]
#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""CronInfo tools.
A library for working with CronInfo records, describing cron entries for an
application. Supports loading the records from yaml.
"""
import logging
import sys
import traceback
try:
import pytz
except ImportError:
pytz = None
from google.appengine.cron import groc
from google.appengine.cron import groctimespecification
from google.appengine.api import appinfo
from google.appengine.api import validation
from google.appengine.api import yaml_builder
from google.appengine.api import yaml_listener
from google.appengine.api import yaml_object
_URL_REGEX = r'^/.*$'
_TIMEZONE_REGEX = r'^.{0,100}$'
_DESCRIPTION_REGEX = ur'^.{0,499}$'
SERVER_ID_RE_STRING = r'(?!-)[a-z\d\-]{1,63}'
SERVER_VERSION_RE_STRING = r'(?!-)[a-z\d\-]{1,100}'
_VERSION_REGEX = r'^(?:(?:(%s):)?)(%s)$' % (SERVER_ID_RE_STRING,
SERVER_VERSION_RE_STRING)
class GrocValidator(validation.Validator):
"""Checks that a schedule is in valid groc format."""
def Validate(self, value, key=None):
"""Validates a schedule."""
if value is None:
raise validation.MissingAttribute('schedule must be specified')
if not isinstance(value, basestring):
raise TypeError('schedule must be a string, not \'%r\''%type(value))
try:
groctimespecification.GrocTimeSpecification(value)
except groc.GrocException, e:
raise validation.ValidationError('schedule \'%s\' failed to parse: %s'%(
value, e.args[0]))
return value
class TimezoneValidator(validation.Validator):
"""Checks that a timezone can be correctly parsed and is known."""
def Validate(self, value, key=None):
"""Validates a timezone."""
if value is None:
return
if not isinstance(value, basestring):
raise TypeError('timezone must be a string, not \'%r\'' % type(value))
if pytz is None:
return value
try:
pytz.timezone(value)
except pytz.UnknownTimeZoneError:
raise validation.ValidationError('timezone \'%s\' is unknown' % value)
except IOError:
return value
except:
unused_e, v, t = sys.exc_info()
logging.warning('pytz raised an unexpected error: %s.\n' % (v) +
'Traceback:\n' + '\n'.join(traceback.format_tb(t)))
raise
return value
CRON = 'cron'
URL = 'url'
SCHEDULE = 'schedule'
TIMEZONE = 'timezone'
DESCRIPTION = 'description'
TARGET = 'target'
class MalformedCronfigurationFile(Exception):
"""Configuration file for Cron is malformed."""
pass
class CronEntry(validation.Validated):
"""A cron entry describes a single cron job."""
ATTRIBUTES = {
URL: _URL_REGEX,
SCHEDULE: GrocValidator(),
TIMEZONE: TimezoneValidator(),
DESCRIPTION: validation.Optional(_DESCRIPTION_REGEX),
TARGET: validation.Optional(_VERSION_REGEX),
}
class CronInfoExternal(validation.Validated):
"""CronInfoExternal describes all cron entries for an application."""
ATTRIBUTES = {
appinfo.APPLICATION: validation.Optional(appinfo.APPLICATION_RE_STRING),
CRON: validation.Optional(validation.Repeated(CronEntry))
}
def LoadSingleCron(cron_info, open_fn=None):
"""Load a cron.yaml file or string and return a CronInfoExternal object."""
builder = yaml_object.ObjectBuilder(CronInfoExternal)
handler = yaml_builder.BuilderHandler(builder)
listener = yaml_listener.EventListener(handler)
listener.Parse(cron_info)
cron_info = handler.GetResults()
if len(cron_info) < 1:
raise MalformedCronfigurationFile('Empty cron configuration.')
if len(cron_info) > 1:
raise MalformedCronfigurationFile('Multiple cron sections '
'in configuration.')
return cron_info[0]