App Engine Python SDK version 1.9.2
[gae.git] / python / google / appengine / api / appinfo.py
blobe91072117fb97733f15aacdadc6ee5bc77d4b3f8
1 #!/usr/bin/env python
3 # Copyright 2007 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
21 """AppInfo tools.
23 Library for working with AppInfo records in memory, store and load from
24 configuration files.
25 """
35 import logging
36 import os
37 import re
38 import string
39 import sys
40 import wsgiref.util
42 if os.environ.get('APPENGINE_RUNTIME') == 'python27':
43 from google.appengine.api import pagespeedinfo
44 from google.appengine.api import validation
45 from google.appengine.api import yaml_builder
46 from google.appengine.api import yaml_listener
47 from google.appengine.api import yaml_object
48 else:
50 from google.appengine.api import pagespeedinfo
51 from google.appengine.api import validation
52 from google.appengine.api import yaml_builder
53 from google.appengine.api import yaml_listener
54 from google.appengine.api import yaml_object
56 from google.appengine.api import appinfo_errors
57 from google.appengine.api import backendinfo
62 _URL_REGEX = r'(?!\^)/.*|\..*|(\(.).*(?!\$).'
63 _FILES_REGEX = r'.+'
64 _URL_ROOT_REGEX = r'/.*'
67 _DELTA_REGEX = r'([0-9]+)([DdHhMm]|[sS]?)'
68 _EXPIRATION_REGEX = r'\s*(%s)(\s+%s)*\s*' % (_DELTA_REGEX, _DELTA_REGEX)
69 _START_PATH = '/_ah/start'
74 _ALLOWED_SERVICES = ['mail', 'mail_bounce', 'xmpp_message', 'xmpp_subscribe',
75 'xmpp_presence', 'xmpp_error', 'channel_presence', 'rest',
76 'warmup']
77 _SERVICE_RE_STRING = '(' + '|'.join(_ALLOWED_SERVICES) + ')'
80 _PAGE_NAME_REGEX = r'^.+$'
83 _EXPIRATION_CONVERSIONS = {
84 'd': 60 * 60 * 24,
85 'h': 60 * 60,
86 'm': 60,
87 's': 1,
92 APP_ID_MAX_LEN = 100
93 MODULE_ID_MAX_LEN = 63
97 MODULE_VERSION_ID_MAX_LEN = 63
98 MAX_URL_MAPS = 100
101 PARTITION_SEPARATOR = '~'
104 DOMAIN_SEPARATOR = ':'
107 VERSION_SEPARATOR = '.'
110 MODULE_SEPARATOR = ':'
113 DEFAULT_MODULE = 'default'
117 PARTITION_RE_STRING = (r'[a-z\d\-]{1,%d}\%s' %
118 (APP_ID_MAX_LEN, PARTITION_SEPARATOR))
119 DOMAIN_RE_STRING = (r'(?!\-)[a-z\d\-\.]{1,%d}%s' %
120 (APP_ID_MAX_LEN, DOMAIN_SEPARATOR))
121 DISPLAY_APP_ID_RE_STRING = r'(?!-)[a-z\d\-]{0,%d}[a-z\d]' % (APP_ID_MAX_LEN - 1)
122 APPLICATION_RE_STRING = (r'(?:%s)?(?:%s)?%s' %
123 (PARTITION_RE_STRING,
124 DOMAIN_RE_STRING,
125 DISPLAY_APP_ID_RE_STRING))
127 MODULE_ID_RE_STRING = r'^(?!-)[a-z\d\-]{0,%d}[a-z\d]$' % (MODULE_ID_MAX_LEN - 1)
134 MODULE_VERSION_ID_RE_STRING = (r'^(?!-)[a-z\d\-]{0,%d}[a-z\d]$' %
135 (MODULE_VERSION_ID_MAX_LEN - 1))
137 _IDLE_INSTANCES_REGEX = r'^([\d]+|automatic)$'
139 _INSTANCES_REGEX = r'^[1-9][\d]*$'
140 _INSTANCE_CLASS_REGEX = r'^([fF](1|2|4|4_1G)|[bB](1|2|4|8|4_1G))$'
142 _CONCURRENT_REQUESTS_REGEX = r'^([1-9]\d*)$'
147 _PENDING_LATENCY_REGEX = r'^(\d+((\.\d{1,3})?s|ms)|automatic)$'
149 _IDLE_TIMEOUT_REGEX = r'^[\d]+(s|m)$'
151 ALTERNATE_HOSTNAME_SEPARATOR = '-dot-'
154 BUILTIN_NAME_PREFIX = 'ah-builtin'
156 RUNTIME_RE_STRING = r'[a-z][a-z0-9]{0,29}'
158 API_VERSION_RE_STRING = r'[\w.]{1,32}'
160 SOURCE_LANGUAGE_RE_STRING = r'[\w.\-]{1,32}'
162 HANDLER_STATIC_FILES = 'static_files'
163 HANDLER_STATIC_DIR = 'static_dir'
164 HANDLER_SCRIPT = 'script'
165 HANDLER_API_ENDPOINT = 'api_endpoint'
167 LOGIN_OPTIONAL = 'optional'
168 LOGIN_REQUIRED = 'required'
169 LOGIN_ADMIN = 'admin'
171 AUTH_FAIL_ACTION_REDIRECT = 'redirect'
172 AUTH_FAIL_ACTION_UNAUTHORIZED = 'unauthorized'
174 DATASTORE_ID_POLICY_LEGACY = 'legacy'
175 DATASTORE_ID_POLICY_DEFAULT = 'default'
177 SECURE_HTTP = 'never'
178 SECURE_HTTPS = 'always'
179 SECURE_HTTP_OR_HTTPS = 'optional'
181 SECURE_DEFAULT = 'default'
183 REQUIRE_MATCHING_FILE = 'require_matching_file'
185 DEFAULT_SKIP_FILES = (r'^(.*/)?('
186 r'(#.*#)|'
187 r'(.*~)|'
188 r'(.*\.py[co])|'
189 r'(.*/RCS/.*)|'
190 r'(\..*)|'
191 r')$')
193 SKIP_NO_FILES = r'(?!)'
195 DEFAULT_NOBUILD_FILES = (r'^$')
198 LOGIN = 'login'
199 AUTH_FAIL_ACTION = 'auth_fail_action'
200 SECURE = 'secure'
201 URL = 'url'
202 POSITION = 'position'
203 POSITION_HEAD = 'head'
204 POSITION_TAIL = 'tail'
205 STATIC_FILES = 'static_files'
206 UPLOAD = 'upload'
207 STATIC_DIR = 'static_dir'
208 MIME_TYPE = 'mime_type'
209 SCRIPT = 'script'
210 EXPIRATION = 'expiration'
211 API_ENDPOINT = 'api_endpoint'
212 HTTP_HEADERS = 'http_headers'
213 APPLICATION_READABLE = 'application_readable'
216 APPLICATION = 'application'
217 MODULE = 'module'
218 AUTOMATIC_SCALING = 'automatic_scaling'
219 MANUAL_SCALING = 'manual_scaling'
220 BASIC_SCALING = 'basic_scaling'
221 VM = 'vm'
222 VM_SETTINGS = 'vm_settings'
223 VM_HEALTH_CHECK = 'vm_health_check'
224 VERSION = 'version'
225 MAJOR_VERSION = 'major_version'
226 MINOR_VERSION = 'minor_version'
227 RUNTIME = 'runtime'
228 API_VERSION = 'api_version'
229 SOURCE_LANGUAGE = 'source_language'
230 BUILTINS = 'builtins'
231 INCLUDES = 'includes'
232 HANDLERS = 'handlers'
233 LIBRARIES = 'libraries'
234 DEFAULT_EXPIRATION = 'default_expiration'
235 SKIP_FILES = 'skip_files'
236 NOBUILD_FILES = 'nobuild_files'
237 SERVICES = 'inbound_services'
238 DERIVED_FILE_TYPE = 'derived_file_type'
239 JAVA_PRECOMPILED = 'java_precompiled'
240 PYTHON_PRECOMPILED = 'python_precompiled'
241 ADMIN_CONSOLE = 'admin_console'
242 ERROR_HANDLERS = 'error_handlers'
243 BACKENDS = 'backends'
244 THREADSAFE = 'threadsafe'
245 DATASTORE_AUTO_ID_POLICY = 'auto_id_policy'
246 API_CONFIG = 'api_config'
247 CODE_LOCK = 'code_lock'
248 ENV_VARIABLES = 'env_variables'
249 PAGESPEED = 'pagespeed'
251 INSTANCE_CLASS = 'instance_class'
253 MINIMUM_PENDING_LATENCY = 'min_pending_latency'
254 MAXIMUM_PENDING_LATENCY = 'max_pending_latency'
255 MINIMUM_IDLE_INSTANCES = 'min_idle_instances'
256 MAXIMUM_IDLE_INSTANCES = 'max_idle_instances'
257 MAXIMUM_CONCURRENT_REQUEST = 'max_concurrent_requests'
260 INSTANCES = 'instances'
263 MAX_INSTANCES = 'max_instances'
264 IDLE_TIMEOUT = 'idle_timeout'
267 PAGES = 'pages'
268 NAME = 'name'
271 ERROR_CODE = 'error_code'
272 FILE = 'file'
273 _ERROR_CODE_REGEX = r'(default|over_quota|dos_api_denial|timeout)'
276 ON = 'on'
277 ON_ALIASES = ['yes', 'y', 'True', 't', '1', 'true']
278 OFF = 'off'
279 OFF_ALIASES = ['no', 'n', 'False', 'f', '0', 'false']
284 ENABLE_HEALTH_CHECK = 'enable_health_check'
285 CHECK_INTERVAL_SEC = 'check_interval_sec'
286 TIMEOUT_SEC = 'timeout_sec'
287 UNHEALTHY_THRESHOLD = 'unhealthy_threshold'
288 HEALTHY_THRESHOLD = 'healthy_threshold'
289 RESTART_THRESHOLD = 'restart_threshold'
290 HOST = 'host'
293 class _VersionedLibrary(object):
294 """A versioned library supported by App Engine."""
296 def __init__(self,
297 name,
298 url,
299 description,
300 supported_versions,
301 default_version=None,
302 deprecated_versions=None,
303 experimental_versions=None):
304 """Initializer for _VersionedLibrary.
306 Args:
307 name: The name of the library e.g. "django".
308 url: The URL for the library's project page e.g.
309 "http://www.djangoproject.com/".
310 description: A short description of the library e.g. "A framework...".
311 supported_versions: A list of supported version names ordered by release
312 date e.g. ["v1", "v2", "v3"].
313 default_version: The version of the library that is enabled by default
314 in the Python 2.7 runtime or None if the library is not available by
315 default e.g. "v1".
316 deprecated_versions: A list of the versions of the library that have been
317 deprecated e.g. ["v1", "v2"].
318 experimental_versions: A list of the versions of the library that are
319 current experimental e.g. ["v1"].
321 self.name = name
322 self.url = url
323 self.description = description
324 self.supported_versions = supported_versions
325 self.default_version = default_version
326 self.deprecated_versions = deprecated_versions or []
327 self.experimental_versions = experimental_versions or []
329 @property
330 def non_deprecated_versions(self):
331 return [version for version in self.supported_versions
332 if version not in self.deprecated_versions]
335 _SUPPORTED_LIBRARIES = [
336 _VersionedLibrary(
337 'django',
338 'http://www.djangoproject.com/',
339 'A full-featured web application framework for Python.',
340 ['1.2', '1.3', '1.4', '1.5'],
341 experimental_versions=['1.5'],
343 _VersionedLibrary(
344 'endpoints',
345 'https://developers.google.com/appengine/docs/python/endpoints/',
346 'Libraries for building APIs in an App Engine application.',
347 ['1.0']),
348 _VersionedLibrary(
349 'jinja2',
350 'http://jinja.pocoo.org/docs/',
351 'A modern and designer friendly templating language for Python.',
352 ['2.6']),
353 _VersionedLibrary(
354 'lxml',
355 'http://lxml.de/',
356 'A Pythonic binding for the C libraries libxml2 and libxslt.',
357 ['2.3', '2.3.5'],
358 experimental_versions=['2.3.5'],
360 _VersionedLibrary(
361 'markupsafe',
362 'http://pypi.python.org/pypi/MarkupSafe',
363 'A XML/HTML/XHTML markup safe string for Python.',
364 ['0.15']),
365 _VersionedLibrary(
366 'matplotlib',
367 'http://matplotlib.org/',
368 'A 2D plotting library which produces publication-quality figures.',
369 ['1.2.0'],
370 experimental_versions=['1.2.0'],
372 _VersionedLibrary(
373 'MySQLdb',
374 'http://mysql-python.sourceforge.net/',
375 'A Python DB API v2.0 compatible interface to MySQL.',
376 ['1.2.4b4'],
377 experimental_versions=['1.2.4b4']
379 _VersionedLibrary(
380 'numpy',
381 'http://numpy.scipy.org/',
382 'A general-purpose library for array-processing.',
383 ['1.6.1']),
384 _VersionedLibrary(
385 'PIL',
386 'http://www.pythonware.com/library/pil/handbook/',
387 'A library for creating and transforming images.',
388 ['1.1.7']),
389 _VersionedLibrary(
390 'protorpc',
391 'https://code.google.com/p/google-protorpc/',
392 'A framework for implementing HTTP-based remote procedure call (RPC) '
393 'services.',
394 ['1.0'],
395 default_version='1.0',
397 _VersionedLibrary(
398 'PyAMF',
399 'http://www.pyamf.org/',
400 'A library that provides (AMF) Action Message Format functionality.',
401 ['0.6.1']),
402 _VersionedLibrary(
403 'pycrypto',
404 'https://www.dlitz.net/software/pycrypto/',
405 'A library of cryptogoogle.appengine._internal.graphy functions such as random number generation.',
406 ['2.3', '2.6'],
408 _VersionedLibrary(
409 'setuptools',
410 'http://pypi.python.org/pypi/setuptools',
411 'A library that provides package and module discovery capabilities.',
412 ['0.6c11']),
413 _VersionedLibrary(
414 'ssl',
415 'http://docs.python.org/dev/library/ssl.html',
416 'The SSL socket wrapper built-in module.',
417 ['2.7'],
418 experimental_versions=['2.7']),
419 _VersionedLibrary(
420 'webapp2',
421 'http://webapp-improved.appspot.com/',
422 'A lightweight Python web framework.',
423 ['2.3', '2.5.1', '2.5.2'],
424 default_version='2.3',
425 deprecated_versions=['2.3']
427 _VersionedLibrary(
428 'webob',
429 'http://www.webob.org/',
430 'A library that provides wrappers around the WSGI request environment.',
431 ['1.1.1', '1.2.3'],
432 default_version='1.1.1',
434 _VersionedLibrary(
435 'yaml',
436 'http://www.yaml.org/',
437 'A library for YAML serialization and deserialization.',
438 ['3.10'],
439 default_version='3.10'
443 _NAME_TO_SUPPORTED_LIBRARY = dict((library.name, library)
444 for library in _SUPPORTED_LIBRARIES)
448 REQUIRED_LIBRARIES = {
449 ('jinja2', '2.6'): [('markupsafe', '0.15'), ('setuptools', '0.6c11')],
450 ('jinja2', 'latest'): [('markupsafe', 'latest'), ('setuptools', 'latest')],
451 ('matplotlib', '1.1.1'): [('numpy', '1.6.1')],
452 ('matplotlib', '1.2.0'): [('numpy', '1.6.1')],
453 ('matplotlib', 'latest'): [('numpy', 'latest')],
456 _USE_VERSION_FORMAT = ('use one of: "%s" or "latest" '
457 '("latest" recommended for development only)')
461 _HTTP_SEPARATOR_CHARS = frozenset('()<>@,;:\\"/[]?={} \t')
462 _HTTP_TOKEN_CHARS = frozenset(string.printable[:-5]) - _HTTP_SEPARATOR_CHARS
463 _HTTP_TOKEN_RE = re.compile('[%s]+$' % re.escape(''.join(_HTTP_TOKEN_CHARS)))
466 _HTTP_REQUEST_HEADERS = frozenset([
467 'accept',
468 'accept-charset',
469 'accept-encoding',
470 'accept-language',
471 'authorization',
472 'expect',
473 'from',
474 'host',
475 'if-match',
476 'if-modified-since',
477 'if-none-match',
478 'if-range',
479 'if-unmodified-since',
480 'max-forwards',
481 'proxy-authorization',
482 'range',
483 'referer',
484 'te',
485 'user-agent',
490 _MAX_COOKIE_LENGTH = 4096
494 _MAX_URL_LENGTH = 2047
497 class HandlerBase(validation.Validated):
498 """Base class for URLMap and ApiConfigHandler."""
499 ATTRIBUTES = {
501 URL: validation.Optional(_URL_REGEX),
502 LOGIN: validation.Options(LOGIN_OPTIONAL,
503 LOGIN_REQUIRED,
504 LOGIN_ADMIN,
505 default=LOGIN_OPTIONAL),
507 AUTH_FAIL_ACTION: validation.Options(AUTH_FAIL_ACTION_REDIRECT,
508 AUTH_FAIL_ACTION_UNAUTHORIZED,
509 default=AUTH_FAIL_ACTION_REDIRECT),
511 SECURE: validation.Options(SECURE_HTTP,
512 SECURE_HTTPS,
513 SECURE_HTTP_OR_HTTPS,
514 SECURE_DEFAULT,
515 default=SECURE_DEFAULT),
518 HANDLER_SCRIPT: validation.Optional(_FILES_REGEX)
522 class HttpHeadersDict(validation.ValidatedDict):
523 """A dict that limits keys and values what http_headers allows.
525 http_headers is an static handler key i.e. it applies to handlers with
526 static_dir or static_files keys. An example of how http_headers is used is
528 handlers:
529 - url: /static
530 static_dir: static
531 http_headers:
532 X-Foo-Header: foo value
533 X-Bar-Header: bar value
537 DISALLOWED_HEADERS = frozenset([
542 'content-encoding',
543 'content-length',
544 'date',
545 'server'
548 MAX_HEADER_LENGTH = 500
549 MAX_HEADER_VALUE_LENGTHS = {
550 'set-cookie': _MAX_COOKIE_LENGTH,
551 'set-cookie2': _MAX_COOKIE_LENGTH,
552 'location': _MAX_URL_LENGTH}
553 MAX_LEN = 500
555 class KeyValidator(validation.Validator):
556 """Ensures that keys in HttpHeadersDict i.e. header names are valid.
558 An instance is used as HttpHeadersDict's KEY_VALIDATOR.
561 def Validate(self, name, unused_key=None):
562 """Returns argument, or raises an exception if it is invalid.
564 HTTP header names are defined by RFC 2616 section 4.2.
566 Args:
567 name: HTTP header field value.
568 unused_key: Unused.
570 Returns:
571 name argument, unchanged.
573 Raises:
574 appinfo_errors.InvalidHttpHeaderName: argument cannot be used as an HTTP
575 header name.
577 original_name = name
580 if isinstance(name, unicode):
581 try:
582 name = name.encode('ascii')
583 except UnicodeEncodeError:
584 raise appinfo_errors.InvalidHttpHeaderName(
585 'HTTP header values must not contain non-ASCII data')
588 name = name.lower()
590 if not _HTTP_TOKEN_RE.match(name):
591 raise appinfo_errors.InvalidHttpHeaderName(
592 'An HTTP header must be a non-empty RFC 2616 token.')
595 if name in _HTTP_REQUEST_HEADERS:
596 raise appinfo_errors.InvalidHttpHeaderName(
597 '%r can only be used in HTTP requests, not responses.'
598 % original_name)
601 if name.startswith('x-appengine'):
602 raise appinfo_errors.InvalidHttpHeaderName(
603 'HTTP header names that begin with X-Appengine are reserved.')
605 if wsgiref.util.is_hop_by_hop(name):
606 raise appinfo_errors.InvalidHttpHeaderName(
607 'Only use end-to-end headers may be used. See RFC 2616 section'
608 ' 13.5.1.')
610 if name in HttpHeadersDict.DISALLOWED_HEADERS:
611 raise appinfo_errors.InvalidHttpHeaderName(
612 '%s is a disallowed header.' % name)
614 return original_name
616 class ValueValidator(validation.Validator):
617 """Ensures that values in HttpHeadersDict i.e. header values are valid.
619 An instance is used as HttpHeadersDict's VALUE_VALIDATOR.
622 def Validate(self, value, key=None):
623 """Returns value, or raises an exception if it is invalid.
625 According to RFC 2616 section 4.2, header field values must consist "of
626 either *TEXT or combinations of token, separators, and quoted-string".
628 TEXT = <any OCTET except CTLs, but including LWS>
630 Args:
631 value: HTTP header field value.
632 key: HTTP header field name.
634 Returns:
635 value argument.
637 Raises:
638 appinfo_errors.InvalidHttpHeaderValue: argument cannot be used as an
639 HTTP header value.
642 if isinstance(value, unicode):
643 try:
644 value = value.encode('ascii')
645 except UnicodeEncodeError:
646 raise appinfo_errors.InvalidHttpHeaderValue(
647 'HTTP header values must not contain non-ASCII data')
650 key = key.lower()
656 printable = set(string.printable[:-5])
657 if not all(char in printable for char in value):
658 raise appinfo_errors.InvalidHttpHeaderValue(
659 'HTTP header field values must consist of printable characters.')
661 HttpHeadersDict.ValueValidator.AssertHeaderNotTooLong(key, value)
663 return value
665 @staticmethod
666 def AssertHeaderNotTooLong(name, value):
667 header_length = len('%s: %s\r\n' % (name, value))
671 if header_length >= HttpHeadersDict.MAX_HEADER_LENGTH:
675 try:
676 max_len = HttpHeadersDict.MAX_HEADER_VALUE_LENGTHS[name]
677 except KeyError:
678 raise appinfo_errors.InvalidHttpHeaderValue(
679 'HTTP header (name + value) is too long.')
683 if len(value) > max_len:
684 insert = name, len(value), max_len
685 raise appinfo_errors.InvalidHttpHeaderValue(
686 '%r header value has length %d, which exceed the maximum allowed,'
687 ' %d.' % insert)
689 KEY_VALIDATOR = KeyValidator()
690 VALUE_VALIDATOR = ValueValidator()
692 def Get(self, header_name):
693 """Gets a header value.
695 Args:
696 header_name: HTTP header name to look for.
698 Returns:
699 A header value that corresponds to header_name. If more than one such
700 value is in self, one of the values is selected arbitrarily, and
701 returned. The selection is not deterministic.
703 for name in self:
704 if name.lower() == header_name.lower():
705 return self[name]
709 def __setitem__(self, key, value):
710 is_addition = self.Get(key) is None
711 if is_addition and len(self) >= self.MAX_LEN:
712 raise appinfo_errors.TooManyHttpHeaders(
713 'Tried to add another header when the current set of HTTP headers'
714 ' already has the maximum allowed number of headers, %d.'
715 % HttpHeadersDict.MAX_LEN)
716 super(HttpHeadersDict, self).__setitem__(key, value)
719 class URLMap(HandlerBase):
720 """Mapping from URLs to handlers.
722 This class acts like something of a union type. Its purpose is to
723 describe a mapping between a set of URLs and their handlers. What
724 handler type a given instance has is determined by which handler-id
725 attribute is used.
727 Each mapping can have one and only one handler type. Attempting to
728 use more than one handler-id attribute will cause an UnknownHandlerType
729 to be raised during validation. Failure to provide any handler-id
730 attributes will cause MissingHandlerType to be raised during validation.
732 The regular expression used by the url field will be used to match against
733 the entire URL path and query string of the request. This means that
734 partial maps will not be matched. Specifying a url, say /admin, is the
735 same as matching against the regular expression '^/admin$'. Don't begin
736 your matching url with ^ or end them with $. These regular expressions
737 won't be accepted and will raise ValueError.
739 Attributes:
740 login: Whether or not login is required to access URL. Defaults to
741 'optional'.
742 secure: Restriction on the protocol which can be used to serve
743 this URL/handler (HTTP, HTTPS or either).
744 url: Regular expression used to fully match against the request URLs path.
745 See Special Cases for using static_dir.
746 static_files: Handler id attribute that maps URL to the appropriate
747 file. Can use back regex references to the string matched to url.
748 upload: Regular expression used by the application configuration
749 program to know which files are uploaded as blobs. It's very
750 difficult to determine this using just the url and static_files
751 so this attribute must be included. Required when defining a
752 static_files mapping.
753 A matching file name must fully match against the upload regex, similar
754 to how url is matched against the request path. Do not begin upload
755 with ^ or end it with $.
756 static_dir: Handler id that maps the provided url to a sub-directory
757 within the application directory. See Special Cases.
758 mime_type: When used with static_files and static_dir the mime-type
759 of files served from those directories are overridden with this
760 value.
761 script: Handler id that maps URLs to scipt handler within the application
762 directory that will run using CGI.
763 position: Used in AppInclude objects to specify whether a handler
764 should be inserted at the beginning of the primary handler list or at the
765 end. If 'tail' is specified, the handler is inserted at the end,
766 otherwise, the handler is inserted at the beginning. This means that
767 'head' is the effective default.
768 expiration: When used with static files and directories, the time delta to
769 use for cache expiration. Has the form '4d 5h 30m 15s', where each letter
770 signifies days, hours, minutes, and seconds, respectively. The 's' for
771 seconds may be omitted. Only one amount must be specified, combining
772 multiple amounts is optional. Example good values: '10', '1d 6h',
773 '1h 30m', '7d 7d 7d', '5m 30'.
774 api_endpoint: Handler id that identifies endpoint as an API endpoint,
775 calls that terminate here will be handled by the api serving framework.
777 Special cases:
778 When defining a static_dir handler, do not use a regular expression
779 in the url attribute. Both the url and static_dir attributes are
780 automatically mapped to these equivalents:
782 <url>/(.*)
783 <static_dir>/\1
785 For example:
787 url: /images
788 static_dir: images_folder
790 Is the same as this static_files declaration:
792 url: /images/(.*)
793 static_files: images_folder/\1
794 upload: images_folder/(.*)
796 ATTRIBUTES = {
799 HANDLER_STATIC_FILES: validation.Optional(_FILES_REGEX),
800 UPLOAD: validation.Optional(_FILES_REGEX),
801 APPLICATION_READABLE: validation.Optional(bool),
804 HANDLER_STATIC_DIR: validation.Optional(_FILES_REGEX),
807 MIME_TYPE: validation.Optional(str),
808 EXPIRATION: validation.Optional(_EXPIRATION_REGEX),
809 REQUIRE_MATCHING_FILE: validation.Optional(bool),
810 HTTP_HEADERS: validation.Optional(HttpHeadersDict),
813 POSITION: validation.Optional(validation.Options(POSITION_HEAD,
814 POSITION_TAIL)),
817 HANDLER_API_ENDPOINT: validation.Optional(validation.Options(
818 (ON, ON_ALIASES),
819 (OFF, OFF_ALIASES))),
822 ATTRIBUTES.update(HandlerBase.ATTRIBUTES)
824 COMMON_FIELDS = set([URL, LOGIN, AUTH_FAIL_ACTION, SECURE])
828 ALLOWED_FIELDS = {
829 HANDLER_STATIC_FILES: (MIME_TYPE, UPLOAD, EXPIRATION,
830 REQUIRE_MATCHING_FILE, HTTP_HEADERS,
831 APPLICATION_READABLE),
832 HANDLER_STATIC_DIR: (MIME_TYPE, EXPIRATION, REQUIRE_MATCHING_FILE,
833 HTTP_HEADERS, APPLICATION_READABLE),
834 HANDLER_SCRIPT: (POSITION),
835 HANDLER_API_ENDPOINT: (POSITION, SCRIPT),
838 def GetHandler(self):
839 """Get handler for mapping.
841 Returns:
842 Value of the handler (determined by handler id attribute).
844 return getattr(self, self.GetHandlerType())
846 def GetHandlerType(self):
847 """Get handler type of mapping.
849 Returns:
850 Handler type determined by which handler id attribute is set.
852 Raises:
853 UnknownHandlerType: when none of the no handler id attributes are set.
855 UnexpectedHandlerAttribute: when an unexpected attribute is set for the
856 discovered handler type.
858 HandlerTypeMissingAttribute: when the handler is missing a
859 required attribute for its handler type.
861 MissingHandlerAttribute: when a URL handler is missing an attribute
865 if getattr(self, HANDLER_API_ENDPOINT) is not None:
867 mapping_type = HANDLER_API_ENDPOINT
868 else:
869 for id_field in URLMap.ALLOWED_FIELDS.iterkeys():
871 if getattr(self, id_field) is not None:
873 mapping_type = id_field
874 break
875 else:
877 raise appinfo_errors.UnknownHandlerType(
878 'Unknown url handler type.\n%s' % str(self))
880 allowed_fields = URLMap.ALLOWED_FIELDS[mapping_type]
884 for attribute in self.ATTRIBUTES.iterkeys():
885 if (getattr(self, attribute) is not None and
886 not (attribute in allowed_fields or
887 attribute in URLMap.COMMON_FIELDS or
888 attribute == mapping_type)):
889 raise appinfo_errors.UnexpectedHandlerAttribute(
890 'Unexpected attribute "%s" for mapping type %s.' %
891 (attribute, mapping_type))
896 if mapping_type == HANDLER_STATIC_FILES and not self.upload:
897 raise appinfo_errors.MissingHandlerAttribute(
898 'Missing "%s" attribute for URL "%s".' % (UPLOAD, self.url))
900 return mapping_type
902 def CheckInitialized(self):
903 """Adds additional checking to make sure handler has correct fields.
905 In addition to normal ValidatedCheck calls GetHandlerType
906 which validates all the handler fields are configured
907 properly.
909 Raises:
910 UnknownHandlerType: when none of the no handler id attributes are set.
911 UnexpectedHandlerAttribute: when an unexpected attribute is set for the
912 discovered handler type.
913 HandlerTypeMissingAttribute: when the handler is missing a required
914 attribute for its handler type.
915 ContentTypeSpecifiedMultipleTimes: when mime_type is inconsistent with
916 http_headers.
918 super(URLMap, self).CheckInitialized()
919 if self.GetHandlerType() in (STATIC_DIR, STATIC_FILES):
931 self.AssertUniqueContentType()
933 def AssertUniqueContentType(self):
934 """Makes sure that self.http_headers is consistent with self.mime_type.
936 Assumes self is a static handler i.e. either self.static_dir or
937 self.static_files is set (to not None).
939 Raises:
940 appinfo_errors.ContentTypeSpecifiedMultipleTimes: Raised when
941 self.http_headers contains a Content-Type header, and self.mime_type is
942 set. For example, the following configuration would be rejected:
944 handlers:
945 - url: /static
946 static_dir: static
947 mime_type: text/html
948 http_headers:
949 content-type: text/html
951 As this example shows, a configuration will be rejected when
952 http_headers and mime_type specify a content type, even when they
953 specify the same content type.
955 used_both_fields = self.mime_type and self.http_headers
956 if not used_both_fields:
957 return
959 content_type = self.http_headers.Get('Content-Type')
960 if content_type is not None:
961 raise appinfo_errors.ContentTypeSpecifiedMultipleTimes(
962 'http_header specified a Content-Type header of %r in a handler that'
963 ' also specified a mime_type of %r.' % (content_type, self.mime_type))
965 def FixSecureDefaults(self):
966 """Force omitted 'secure: ...' handler fields to 'secure: optional'.
968 The effect is that handler.secure is never equal to the (nominal)
969 default.
971 See http://b/issue?id=2073962.
973 if self.secure == SECURE_DEFAULT:
974 self.secure = SECURE_HTTP_OR_HTTPS
976 def WarnReservedURLs(self):
977 """Generates a warning for reserved URLs.
979 See:
980 https://developers.google.com/appengine/docs/python/config/appconfig#Reserved_URLs
982 if self.url == '/form':
983 logging.warning(
984 'The URL path "/form" is reserved and will not be matched.')
986 def ErrorOnPositionForAppInfo(self):
987 """Raises an error if position is specified outside of AppInclude objects.
989 Raises:
990 PositionUsedInAppYamlHandler: when position attribute is specified for an
991 app.yaml file instead of an include.yaml file.
993 if self.position:
994 raise appinfo_errors.PositionUsedInAppYamlHandler(
995 'The position attribute was specified for this handler, but this is '
996 'an app.yaml file. Position attribute is only valid for '
997 'include.yaml files.')
1000 class AdminConsolePage(validation.Validated):
1001 """Class representing admin console page in AdminConsole object.
1003 ATTRIBUTES = {
1004 URL: _URL_REGEX,
1005 NAME: _PAGE_NAME_REGEX,
1009 class AdminConsole(validation.Validated):
1010 """Class representing admin console directives in application info.
1012 ATTRIBUTES = {
1013 PAGES: validation.Optional(validation.Repeated(AdminConsolePage)),
1016 @classmethod
1017 def Merge(cls, adminconsole_one, adminconsole_two):
1018 """Return the result of merging two AdminConsole objects."""
1027 if not adminconsole_one or not adminconsole_two:
1028 return adminconsole_one or adminconsole_two
1030 if adminconsole_one.pages:
1031 if adminconsole_two.pages:
1032 adminconsole_one.pages.extend(adminconsole_two.pages)
1033 else:
1034 adminconsole_one.pages = adminconsole_two.pages
1036 return adminconsole_one
1039 class ErrorHandlers(validation.Validated):
1040 """Class representing error handler directives in application info.
1042 ATTRIBUTES = {
1043 ERROR_CODE: validation.Optional(_ERROR_CODE_REGEX),
1044 FILE: _FILES_REGEX,
1045 MIME_TYPE: validation.Optional(str),
1049 class BuiltinHandler(validation.Validated):
1050 """Class representing builtin handler directives in application info.
1052 Permits arbitrary keys but their values must be described by the
1053 validation.Options object returned by ATTRIBUTES.
1083 class DynamicAttributes(dict):
1084 """Provide a dictionary object that will always claim to have a key.
1086 This dictionary returns a fixed value for any get operation. The fixed
1087 value passed in as a constructor parameter should be a
1088 validation.Validated object.
1091 def __init__(self, return_value, **parameters):
1092 self.__return_value = return_value
1093 dict.__init__(self, parameters)
1095 def __contains__(self, _):
1096 return True
1098 def __getitem__(self, _):
1099 return self.__return_value
1101 ATTRIBUTES = DynamicAttributes(
1102 validation.Optional(validation.Options((ON, ON_ALIASES),
1103 (OFF, OFF_ALIASES))))
1105 def __init__(self, **attributes):
1106 """Ensure that all BuiltinHandler objects at least have attribute 'default'.
1108 self.builtin_name = ''
1109 super(BuiltinHandler, self).__init__(**attributes)
1111 def __setattr__(self, key, value):
1112 """Permit ATTRIBUTES.iteritems() to return set of items that have values.
1114 Whenever validate calls iteritems(), it is always called on ATTRIBUTES,
1115 not on __dict__, so this override is important to ensure that functions
1116 such as ToYAML() return the correct set of keys.
1118 Raises:
1119 MultipleBuiltinsSpecified: when more than one builtin is defined in a list
1120 element.
1122 if key == 'builtin_name':
1123 object.__setattr__(self, key, value)
1124 elif not self.builtin_name:
1125 self.ATTRIBUTES[key] = ''
1126 self.builtin_name = key
1127 super(BuiltinHandler, self).__setattr__(key, value)
1128 else:
1133 raise appinfo_errors.MultipleBuiltinsSpecified(
1134 'More than one builtin defined in list element. Each new builtin '
1135 'should be prefixed by "-".')
1137 def __getattr__(self, key):
1138 if key.startswith('_'):
1141 raise AttributeError
1142 return None
1144 def ToDict(self):
1145 """Convert BuiltinHander object to a dictionary.
1147 Returns:
1148 dictionary of the form: {builtin_handler_name: on/off}
1150 return {self.builtin_name: getattr(self, self.builtin_name)}
1152 @classmethod
1153 def IsDefined(cls, builtins_list, builtin_name):
1154 """Find if a builtin is defined in a given list of builtin handler objects.
1156 Args:
1157 builtins_list: list of BuiltinHandler objects (typically yaml.builtins)
1158 builtin_name: name of builtin to find whether or not it is defined
1160 Returns:
1161 true if builtin_name is defined by a member of builtins_list,
1162 false otherwise
1164 for b in builtins_list:
1165 if b.builtin_name == builtin_name:
1166 return True
1167 return False
1169 @classmethod
1170 def ListToTuples(cls, builtins_list):
1171 """Converts a list of BuiltinHandler objects to a list of (name, status)."""
1172 return [(b.builtin_name, getattr(b, b.builtin_name)) for b in builtins_list]
1174 @classmethod
1175 def Validate(cls, builtins_list, runtime=None):
1176 """Verify that all BuiltinHandler objects are valid and not repeated.
1178 Args:
1179 builtins_list: list of BuiltinHandler objects to validate.
1180 runtime: if set then warnings are generated for builtins that have been
1181 deprecated in the given runtime.
1183 Raises:
1184 InvalidBuiltinFormat: if the name of a Builtinhandler object
1185 cannot be determined.
1186 DuplicateBuiltinsSpecified: if a builtin handler name is used
1187 more than once in the list.
1189 seen = set()
1190 for b in builtins_list:
1191 if not b.builtin_name:
1192 raise appinfo_errors.InvalidBuiltinFormat(
1193 'Name of builtin for list object %s could not be determined.'
1194 % b)
1195 if b.builtin_name in seen:
1196 raise appinfo_errors.DuplicateBuiltinsSpecified(
1197 'Builtin %s was specified more than once in one yaml file.'
1198 % b.builtin_name)
1205 if b.builtin_name == 'datastore_admin' and runtime == 'python':
1206 logging.warning(
1207 'The datastore_admin builtin is deprecated. You can find '
1208 'information on how to enable it through the Administrative '
1209 'Console here: '
1210 'http://developers.google.com/appengine/docs/adminconsole/'
1211 'datastoreadmin.html')
1212 elif b.builtin_name == 'mapreduce' and runtime == 'python':
1213 logging.warning(
1214 'The mapreduce builtin is deprecated. You can find more '
1215 'information on how to configure and use it here: '
1216 'http://developers.google.com/appengine/docs/python/dataprocessing/'
1217 'overview.html')
1219 seen.add(b.builtin_name)
1222 class ApiConfigHandler(HandlerBase):
1223 """Class representing api_config handler directives in application info."""
1224 ATTRIBUTES = HandlerBase.ATTRIBUTES
1225 ATTRIBUTES.update({
1227 URL: validation.Regex(_URL_REGEX),
1228 HANDLER_SCRIPT: validation.Regex(_FILES_REGEX)
1232 class Library(validation.Validated):
1233 """Class representing the configuration of a single library."""
1235 ATTRIBUTES = {'name': validation.Type(str),
1236 'version': validation.Type(str)}
1238 def CheckInitialized(self):
1239 """Raises if the library configuration is not valid."""
1240 super(Library, self).CheckInitialized()
1241 if self.name not in _NAME_TO_SUPPORTED_LIBRARY:
1242 raise appinfo_errors.InvalidLibraryName(
1243 'the library "%s" is not supported' % self.name)
1245 supported_library = _NAME_TO_SUPPORTED_LIBRARY[self.name]
1246 if self.version != 'latest':
1247 if self.version not in supported_library.supported_versions:
1248 raise appinfo_errors.InvalidLibraryVersion(
1249 ('%s version "%s" is not supported, ' + _USE_VERSION_FORMAT) % (
1250 self.name,
1251 self.version,
1252 '", "'.join(supported_library.non_deprecated_versions)))
1253 elif self.version in supported_library.deprecated_versions:
1254 logging.warning(
1255 ('%s version "%s" is deprecated, ' + _USE_VERSION_FORMAT) % (
1256 self.name,
1257 self.version,
1258 '", "'.join(supported_library.non_deprecated_versions)))
1261 class AutomaticScaling(validation.Validated):
1262 """Class representing automatic scaling settings in the AppInfoExternal."""
1263 ATTRIBUTES = {
1264 MINIMUM_IDLE_INSTANCES: validation.Optional(_IDLE_INSTANCES_REGEX),
1265 MAXIMUM_IDLE_INSTANCES: validation.Optional(_IDLE_INSTANCES_REGEX),
1266 MINIMUM_PENDING_LATENCY: validation.Optional(_PENDING_LATENCY_REGEX),
1267 MAXIMUM_PENDING_LATENCY: validation.Optional(_PENDING_LATENCY_REGEX),
1268 MAXIMUM_CONCURRENT_REQUEST: validation.Optional(
1269 _CONCURRENT_REQUESTS_REGEX),
1273 class ManualScaling(validation.Validated):
1274 """Class representing manual scaling settings in the AppInfoExternal."""
1275 ATTRIBUTES = {
1276 INSTANCES: validation.Regex(_INSTANCES_REGEX),
1280 class BasicScaling(validation.Validated):
1281 """Class representing basic scaling settings in the AppInfoExternal."""
1282 ATTRIBUTES = {
1283 MAX_INSTANCES: validation.Regex(_INSTANCES_REGEX),
1284 IDLE_TIMEOUT: validation.Optional(_IDLE_TIMEOUT_REGEX),
1288 class VmSettings(validation.ValidatedDict):
1289 """Class for VM settings.
1291 We don't validate these further because the feature is in flux.
1294 KEY_VALIDATOR = validation.Regex('[a-zA-Z_][a-zA-Z0-9_]*')
1295 VALUE_VALIDATOR = str
1297 @classmethod
1298 def Merge(cls, vm_settings_one, vm_settings_two):
1300 result_vm_settings = (vm_settings_two or {}).copy()
1304 result_vm_settings.update(vm_settings_one or {})
1305 return VmSettings(**result_vm_settings) if result_vm_settings else None
1308 class EnvironmentVariables(validation.ValidatedDict):
1309 """Class representing a mapping of environment variable key value pairs."""
1311 KEY_VALIDATOR = validation.Regex('[a-zA-Z_][a-zA-Z0-9_]*')
1312 VALUE_VALIDATOR = str
1314 @classmethod
1315 def Merge(cls, env_variables_one, env_variables_two):
1316 """Merges to EnvironmentVariables instances.
1318 Args:
1319 env_variables_one: The first EnvironmentVariables instance or None.
1320 env_variables_two: The second EnvironmentVariables instance or None.
1322 Returns:
1323 The merged EnvironmentVariables instance, or None if both input instances
1324 are None or empty.
1326 If a variable is specified by both instances, the value from
1327 env_variables_two is used.
1330 result_env_variables = (env_variables_one or {}).copy()
1331 result_env_variables.update(env_variables_two or {})
1332 return (EnvironmentVariables(**result_env_variables)
1333 if result_env_variables else None)
1336 def VmSafeSetRuntime(appyaml, runtime):
1337 """Sets the runtime while respecting vm runtimes rules for runtime settings.
1339 Args:
1340 appyaml: AppInfoExternal instance, which will be modified.
1341 runtime: The runtime to use.
1343 Returns:
1344 The passed in appyaml (which has been modified).
1346 if appyaml.vm:
1347 if not appyaml.vm_settings:
1348 appyaml.vm_settings = VmSettings()
1351 appyaml.vm_settings['vm_runtime'] = runtime
1352 appyaml.runtime = 'vm'
1353 else:
1354 appyaml.runtime = runtime
1355 return appyaml
1358 def NormalizeVmSettings(appyaml):
1359 """Normalize Vm settings.
1361 Args:
1362 appyaml: AppInfoExternal instance.
1364 Returns:
1365 Normalized app yaml.
1373 if appyaml.vm:
1374 if not appyaml.vm_settings:
1375 appyaml.vm_settings = VmSettings()
1376 if 'vm_runtime' not in appyaml.vm_settings:
1377 appyaml = VmSafeSetRuntime(appyaml, appyaml.runtime)
1378 return appyaml
1381 class VmHealthCheck(validation.Validated):
1382 """Class representing the configuration of a single library."""
1384 ATTRIBUTES = {
1385 ENABLE_HEALTH_CHECK: validation.Optional(validation.TYPE_BOOL),
1386 CHECK_INTERVAL_SEC: validation.Optional(validation.Range(0, sys.maxint)),
1387 TIMEOUT_SEC: validation.Optional(validation.Range(0, sys.maxint)),
1388 UNHEALTHY_THRESHOLD: validation.Optional(validation.Range(0, sys.maxint)),
1389 HEALTHY_THRESHOLD: validation.Optional(validation.Range(0, sys.maxint)),
1390 RESTART_THRESHOLD: validation.Optional(validation.Range(0, sys.maxint)),
1391 HOST: validation.Optional(validation.TYPE_STR)}
1394 class AppInclude(validation.Validated):
1395 """Class representing the contents of an included app.yaml file.
1397 Used for both builtins and includes directives.
1403 ATTRIBUTES = {
1404 BUILTINS: validation.Optional(validation.Repeated(BuiltinHandler)),
1405 INCLUDES: validation.Optional(validation.Type(list)),
1406 HANDLERS: validation.Optional(validation.Repeated(URLMap)),
1407 ADMIN_CONSOLE: validation.Optional(AdminConsole),
1408 MANUAL_SCALING: validation.Optional(ManualScaling),
1409 VM: validation.Optional(bool),
1410 VM_SETTINGS: validation.Optional(VmSettings),
1411 ENV_VARIABLES: validation.Optional(EnvironmentVariables),
1412 SKIP_FILES: validation.RegexStr(default=SKIP_NO_FILES),
1417 @classmethod
1418 def MergeManualScaling(cls, appinclude_one, appinclude_two):
1419 """Takes the greater of <manual_scaling.instances> from the args.
1421 Note that appinclude_one is mutated to be the merged result in this process.
1423 Also, this function needs to be updated if ManualScaling gets additional
1424 fields.
1426 Args:
1427 appinclude_one: object one to merge. Must have a "manual_scaling" field
1428 which contains a ManualScaling().
1429 appinclude_two: object two to merge. Must have a "manual_scaling" field
1430 which contains a ManualScaling().
1432 Returns:
1433 Object that is the result of merging
1434 appinclude_one.manual_scaling.instances and
1435 appinclude_two.manual_scaling.instances. I.e., <appinclude_one>
1436 after the mutations are complete.
1439 def _Instances(appinclude):
1440 if appinclude.manual_scaling:
1441 if appinclude.manual_scaling.instances:
1442 return int(appinclude.manual_scaling.instances)
1443 return None
1447 instances = max(_Instances(appinclude_one), _Instances(appinclude_two))
1448 if instances is not None:
1449 appinclude_one.manual_scaling = ManualScaling(instances=str(instances))
1450 return appinclude_one
1452 @classmethod
1453 def _CommonMergeOps(cls, one, two):
1454 """This function performs common merge operations."""
1456 AppInclude.MergeManualScaling(one, two)
1459 one.admin_console = AdminConsole.Merge(one.admin_console,
1460 two.admin_console)
1464 one.vm = two.vm or one.vm
1467 one.vm_settings = VmSettings.Merge(one.vm_settings,
1468 two.vm_settings)
1472 one.env_variables = EnvironmentVariables.Merge(one.env_variables,
1473 two.env_variables)
1475 one.skip_files = cls.MergeSkipFiles(one.skip_files, two.skip_files)
1477 return one
1479 @classmethod
1480 def MergeAppYamlAppInclude(cls, appyaml, appinclude):
1481 """This function merges an app.yaml file with referenced builtins/includes.
1487 if not appinclude:
1488 return appyaml
1491 if appinclude.handlers:
1492 tail = appyaml.handlers or []
1493 appyaml.handlers = []
1495 for h in appinclude.handlers:
1496 if not h.position or h.position == 'head':
1497 appyaml.handlers.append(h)
1498 else:
1499 tail.append(h)
1503 h.position = None
1505 appyaml.handlers.extend(tail)
1507 appyaml = cls._CommonMergeOps(appyaml, appinclude)
1508 return NormalizeVmSettings(appyaml)
1510 @classmethod
1511 def MergeAppIncludes(cls, appinclude_one, appinclude_two):
1512 """This function merges the non-referential state of the provided AppInclude
1513 objects. That is, builtins and includes directives are not preserved, but
1514 any static objects are copied into an aggregate AppInclude object that
1515 preserves the directives of both provided AppInclude objects.
1517 Note that appinclude_one is mutated to be the merged result in this process.
1519 Args:
1520 appinclude_one: object one to merge
1521 appinclude_two: object two to merge
1523 Returns:
1524 AppInclude object that is the result of merging the static directives of
1525 appinclude_one and appinclude_two. I.e., <appinclude_one> after the
1526 mutations are complete.
1531 if not appinclude_one or not appinclude_two:
1532 return appinclude_one or appinclude_two
1536 if appinclude_one.handlers:
1537 if appinclude_two.handlers:
1538 appinclude_one.handlers.extend(appinclude_two.handlers)
1539 else:
1540 appinclude_one.handlers = appinclude_two.handlers
1542 return cls._CommonMergeOps(appinclude_one, appinclude_two)
1544 @staticmethod
1545 def MergeSkipFiles(skip_files_one, skip_files_two):
1546 if skip_files_one == SKIP_NO_FILES:
1547 return skip_files_two
1548 if skip_files_two == SKIP_NO_FILES:
1549 return skip_files_one
1550 return validation.RegexStr().Validate(
1551 [skip_files_one, skip_files_two], SKIP_FILES)
1556 class AppInfoExternal(validation.Validated):
1557 """Class representing users application info.
1559 This class is passed to a yaml_object builder to provide the validation
1560 for the application information file format parser.
1562 Attributes:
1563 application: Unique identifier for application.
1564 version: Application's major version.
1565 runtime: Runtime used by application.
1566 api_version: Which version of APIs to use.
1567 source_language: Optional specification of the source language.
1568 For example we specify "php-quercus" if this is a Java app
1569 that was generated from PHP source using Quercus
1570 handlers: List of URL handlers.
1571 default_expiration: Default time delta to use for cache expiration for
1572 all static files, unless they have their own specific 'expiration' set.
1573 See the URLMap.expiration field's documentation for more information.
1574 skip_files: An re object. Files that match this regular expression will
1575 not be uploaded by appcfg.py. For example:
1576 skip_files: |
1577 .svn.*|
1578 #.*#
1579 nobuild_files: An re object. Files that match this regular expression will
1580 not be built into the app. Go only.
1581 api_config: URL root and script/servlet path for enhanced api serving
1584 ATTRIBUTES = {
1587 APPLICATION: validation.Optional(APPLICATION_RE_STRING),
1588 MODULE: validation.Optional(MODULE_ID_RE_STRING),
1589 VERSION: validation.Optional(MODULE_VERSION_ID_RE_STRING),
1590 RUNTIME: RUNTIME_RE_STRING,
1593 API_VERSION: API_VERSION_RE_STRING,
1594 INSTANCE_CLASS: validation.Optional(_INSTANCE_CLASS_REGEX),
1595 SOURCE_LANGUAGE: validation.Optional(
1596 validation.Regex(SOURCE_LANGUAGE_RE_STRING)),
1597 AUTOMATIC_SCALING: validation.Optional(AutomaticScaling),
1598 MANUAL_SCALING: validation.Optional(ManualScaling),
1599 BASIC_SCALING: validation.Optional(BasicScaling),
1600 VM: validation.Optional(bool),
1601 VM_SETTINGS: validation.Optional(VmSettings),
1602 VM_HEALTH_CHECK: validation.Optional(VmHealthCheck),
1603 BUILTINS: validation.Optional(validation.Repeated(BuiltinHandler)),
1604 INCLUDES: validation.Optional(validation.Type(list)),
1605 HANDLERS: validation.Optional(validation.Repeated(URLMap)),
1606 LIBRARIES: validation.Optional(validation.Repeated(Library)),
1608 SERVICES: validation.Optional(validation.Repeated(
1609 validation.Regex(_SERVICE_RE_STRING))),
1610 DEFAULT_EXPIRATION: validation.Optional(_EXPIRATION_REGEX),
1611 SKIP_FILES: validation.RegexStr(default=DEFAULT_SKIP_FILES),
1612 NOBUILD_FILES: validation.RegexStr(default=DEFAULT_NOBUILD_FILES),
1613 DERIVED_FILE_TYPE: validation.Optional(validation.Repeated(
1614 validation.Options(JAVA_PRECOMPILED, PYTHON_PRECOMPILED))),
1615 ADMIN_CONSOLE: validation.Optional(AdminConsole),
1616 ERROR_HANDLERS: validation.Optional(validation.Repeated(ErrorHandlers)),
1617 BACKENDS: validation.Optional(validation.Repeated(
1618 backendinfo.BackendEntry)),
1619 THREADSAFE: validation.Optional(bool),
1620 DATASTORE_AUTO_ID_POLICY: validation.Optional(
1621 validation.Options(DATASTORE_ID_POLICY_LEGACY,
1622 DATASTORE_ID_POLICY_DEFAULT)),
1623 API_CONFIG: validation.Optional(ApiConfigHandler),
1624 CODE_LOCK: validation.Optional(bool),
1625 ENV_VARIABLES: validation.Optional(EnvironmentVariables),
1626 PAGESPEED: validation.Optional(pagespeedinfo.PagespeedEntry),
1633 _skip_runtime_checks = False
1635 def CheckInitialized(self):
1636 """Performs non-regex-based validation.
1638 The following are verified:
1639 - At least one url mapping is provided in the URL mappers.
1640 - Number of url mappers doesn't exceed MAX_URL_MAPS.
1641 - Major version does not contain the string -dot-.
1642 - If api_endpoints are defined, an api_config stanza must be defined.
1643 - If the runtime is python27 and threadsafe is set, then no CGI handlers
1644 can be used.
1645 - That the version name doesn't start with BUILTIN_NAME_PREFIX
1647 Raises:
1648 DuplicateLibrary: if the name library name is specified more than once.
1649 MissingURLMapping: if no URLMap object is present in the object.
1650 TooManyURLMappings: if there are too many URLMap entries.
1651 MissingApiConfig: if api_endpoints exist without an api_config.
1652 MissingThreadsafe: if threadsafe is not set but the runtime requires it.
1653 ThreadsafeWithCgiHandler: if the runtime is python27, threadsafe is set
1654 and CGI handlers are specified.
1655 TooManyScalingSettingsError: if more than one scaling settings block is
1656 present.
1657 RuntimeDoesNotSupportLibraries: if libraries clause is used for a runtime
1658 that does not support it (e.g. python25).
1660 super(AppInfoExternal, self).CheckInitialized()
1661 if not self.handlers and not self.builtins and not self.includes:
1662 raise appinfo_errors.MissingURLMapping(
1663 'No URLMap entries found in application configuration')
1664 if self.handlers and len(self.handlers) > MAX_URL_MAPS:
1665 raise appinfo_errors.TooManyURLMappings(
1666 'Found more than %d URLMap entries in application configuration' %
1667 MAX_URL_MAPS)
1669 if (self.threadsafe is None and
1670 self.runtime == 'python27' and
1671 not self._skip_runtime_checks):
1672 raise appinfo_errors.MissingThreadsafe(
1673 'threadsafe must be present and set to either "yes" or "no"')
1675 if self.auto_id_policy == DATASTORE_ID_POLICY_LEGACY:
1676 datastore_auto_ids_url = ('http://developers.google.com/'
1677 'appengine/docs/python/datastore/'
1678 'entities#Kinds_and_Identifiers')
1679 appcfg_auto_ids_url = ('http://developers.google.com/appengine/docs/'
1680 'python/config/appconfig#auto_id_policy')
1681 logging.warning(
1682 "You have set the datastore auto_id_policy to 'legacy'. It is "
1683 "recommended that you select 'default' instead.\n"
1684 "Legacy auto ids are deprecated. You can continue to allocate\n"
1685 "legacy ids manually using the allocate_ids() API functions.\n"
1686 "For more information see:\n"
1687 + datastore_auto_ids_url + '\n' + appcfg_auto_ids_url + '\n')
1689 if self.libraries:
1690 vm_runtime_python27 = (
1691 self.runtime == 'vm' and
1692 hasattr(self, 'vm_settings') and
1693 self.vm_settings['vm_runtime'] == 'python27')
1694 if not self._skip_runtime_checks and not (
1695 vm_runtime_python27 or self.runtime == 'python27'):
1696 raise appinfo_errors.RuntimeDoesNotSupportLibraries(
1697 'libraries entries are only supported by the "python27" runtime')
1699 library_names = [library.name for library in self.libraries]
1700 for library_name in library_names:
1701 if library_names.count(library_name) > 1:
1702 raise appinfo_errors.DuplicateLibrary(
1703 'Duplicate library entry for %s' % library_name)
1705 if self.version and self.version.find(ALTERNATE_HOSTNAME_SEPARATOR) != -1:
1706 raise validation.ValidationError(
1707 'Version "%s" cannot contain the string "%s"' % (
1708 self.version, ALTERNATE_HOSTNAME_SEPARATOR))
1709 if self.version and self.version.startswith(BUILTIN_NAME_PREFIX):
1710 raise validation.ValidationError(
1711 ('Version "%s" cannot start with "%s" because it is a '
1712 'reserved version name prefix.') % (self.version,
1713 BUILTIN_NAME_PREFIX))
1714 if self.handlers:
1715 api_endpoints = [handler.url for handler in self.handlers
1716 if handler.GetHandlerType() == HANDLER_API_ENDPOINT]
1717 if api_endpoints and not self.api_config:
1718 raise appinfo_errors.MissingApiConfig(
1719 'An api_endpoint handler was specified, but the required '
1720 'api_config stanza was not configured.')
1721 if (self.threadsafe and
1722 self.runtime == 'python27' and
1723 not self._skip_runtime_checks):
1724 for handler in self.handlers:
1725 if (handler.script and (handler.script.endswith('.py') or
1726 '/' in handler.script)):
1727 raise appinfo_errors.ThreadsafeWithCgiHandler(
1728 'threadsafe cannot be enabled with CGI handler: %s' %
1729 handler.script)
1730 if sum([bool(self.automatic_scaling),
1731 bool(self.manual_scaling),
1732 bool(self.basic_scaling)]) > 1:
1733 raise appinfo_errors.TooManyScalingSettingsError(
1734 "There may be only one of 'automatic_scaling', 'manual_scaling', "
1735 "or 'basic_scaling'.")
1738 def GetAllLibraries(self):
1739 """Returns a list of all Library instances active for this configuration.
1741 Returns:
1742 The list of active Library instances for this configuration. This includes
1743 directly-specified libraries as well as any required dependencies.
1745 if not self.libraries:
1746 return []
1748 library_names = set(library.name for library in self.libraries)
1749 required_libraries = []
1751 for library in self.libraries:
1752 for required_name, required_version in REQUIRED_LIBRARIES.get(
1753 (library.name, library.version), []):
1754 if required_name not in library_names:
1755 required_libraries.append(Library(name=required_name,
1756 version=required_version))
1758 return [Library(**library.ToDict())
1759 for library in self.libraries + required_libraries]
1761 def GetNormalizedLibraries(self):
1762 """Returns a list of normalized Library instances for this configuration.
1764 Returns:
1765 The list of active Library instances for this configuration. This includes
1766 directly-specified libraries, their required dependencies as well as any
1767 libraries enabled by default. Any libraries with "latest" as their version
1768 will be replaced with the latest available version.
1770 libraries = self.GetAllLibraries()
1771 enabled_libraries = set(library.name for library in libraries)
1772 for library in _SUPPORTED_LIBRARIES:
1773 if library.default_version and library.name not in enabled_libraries:
1774 libraries.append(Library(name=library.name,
1775 version=library.default_version))
1776 for library in libraries:
1777 if library.version == 'latest':
1778 library.version = _NAME_TO_SUPPORTED_LIBRARY[
1779 library.name].supported_versions[-1]
1780 return libraries
1782 def ApplyBackendSettings(self, backend_name):
1783 """Applies settings from the indicated backend to the AppInfoExternal.
1785 Backend entries may contain directives that modify other parts of the
1786 app.yaml, such as the 'start' directive, which adds a handler for the start
1787 request. This method performs those modifications.
1789 Args:
1790 backend_name: The name of a backend defined in 'backends'.
1792 Raises:
1793 BackendNotFound: if the indicated backend was not listed in 'backends'.
1794 DuplicateBackend: if backend is found more than once in 'backends'.
1796 if backend_name is None:
1797 return
1799 if self.backends is None:
1800 raise appinfo_errors.BackendNotFound
1802 self.version = backend_name
1804 match = None
1805 for backend in self.backends:
1806 if backend.name != backend_name:
1807 continue
1808 if match:
1809 raise appinfo_errors.DuplicateBackend
1810 else:
1811 match = backend
1813 if match is None:
1814 raise appinfo_errors.BackendNotFound
1816 if match.start is None:
1817 return
1819 start_handler = URLMap(url=_START_PATH, script=match.start)
1820 self.handlers.insert(0, start_handler)
1822 def GetEffectiveRuntime(self):
1823 """Returns the app's runtime, resolving VMs to the underlying vm_runtime.
1825 Returns:
1826 The effective runtime: the value of vm_settings.vm_runtime if runtime is
1827 "vm", or runtime otherwise.
1829 if self.runtime == 'vm' and hasattr(self, 'vm_settings'):
1830 return self.vm_settings.get('vm_runtime')
1831 return self.runtime
1834 def ValidateHandlers(handlers, is_include_file=False):
1835 """Validates a list of handler (URLMap) objects.
1837 Args:
1838 handlers: A list of a handler (URLMap) objects.
1839 is_include_file: If true, indicates the we are performing validation
1840 for handlers in an AppInclude file, which may contain special directives.
1842 if not handlers:
1843 return
1845 for handler in handlers:
1846 handler.FixSecureDefaults()
1847 handler.WarnReservedURLs()
1848 if not is_include_file:
1849 handler.ErrorOnPositionForAppInfo()
1852 def LoadSingleAppInfo(app_info):
1853 """Load a single AppInfo object where one and only one is expected.
1855 Args:
1856 app_info: A file-like object or string. If it is a string, parse it as
1857 a configuration file. If it is a file-like object, read in data and
1858 parse.
1860 Returns:
1861 An instance of AppInfoExternal as loaded from a YAML file.
1863 Raises:
1864 ValueError: if a specified service is not valid.
1865 EmptyConfigurationFile: when there are no documents in YAML file.
1866 MultipleConfigurationFile: when there is more than one document in YAML
1867 file.
1868 DuplicateBackend: if backend is found more than once in 'backends'.
1870 builder = yaml_object.ObjectBuilder(AppInfoExternal)
1871 handler = yaml_builder.BuilderHandler(builder)
1872 listener = yaml_listener.EventListener(handler)
1873 listener.Parse(app_info)
1875 app_infos = handler.GetResults()
1876 if len(app_infos) < 1:
1877 raise appinfo_errors.EmptyConfigurationFile()
1878 if len(app_infos) > 1:
1879 raise appinfo_errors.MultipleConfigurationFile()
1881 appyaml = app_infos[0]
1882 ValidateHandlers(appyaml.handlers)
1883 if appyaml.builtins:
1884 BuiltinHandler.Validate(appyaml.builtins, appyaml.runtime)
1886 return NormalizeVmSettings(appyaml)
1889 class AppInfoSummary(validation.Validated):
1890 """This class contains only basic summary information about an app.
1892 It is used to pass back information about the newly created app to users
1893 after a new version has been created.
1900 ATTRIBUTES = {
1901 APPLICATION: APPLICATION_RE_STRING,
1902 MAJOR_VERSION: MODULE_VERSION_ID_RE_STRING,
1903 MINOR_VERSION: validation.TYPE_LONG
1907 def LoadAppInclude(app_include):
1908 """Load a single AppInclude object where one and only one is expected.
1910 Args:
1911 app_include: A file-like object or string. If it is a string, parse it as
1912 a configuration file. If it is a file-like object, read in data and
1913 parse.
1915 Returns:
1916 An instance of AppInclude as loaded from a YAML file.
1918 Raises:
1919 EmptyConfigurationFile: when there are no documents in YAML file.
1920 MultipleConfigurationFile: when there is more than one document in YAML
1921 file.
1923 builder = yaml_object.ObjectBuilder(AppInclude)
1924 handler = yaml_builder.BuilderHandler(builder)
1925 listener = yaml_listener.EventListener(handler)
1926 listener.Parse(app_include)
1928 includes = handler.GetResults()
1929 if len(includes) < 1:
1930 raise appinfo_errors.EmptyConfigurationFile()
1931 if len(includes) > 1:
1932 raise appinfo_errors.MultipleConfigurationFile()
1934 includeyaml = includes[0]
1935 if includeyaml.handlers:
1936 for handler in includeyaml.handlers:
1937 handler.FixSecureDefaults()
1938 handler.WarnReservedURLs()
1939 if includeyaml.builtins:
1940 BuiltinHandler.Validate(includeyaml.builtins)
1942 return includeyaml
1945 def ParseExpiration(expiration):
1946 """Parses an expiration delta string.
1948 Args:
1949 expiration: String that matches _DELTA_REGEX.
1951 Returns:
1952 Time delta in seconds.
1954 delta = 0
1955 for match in re.finditer(_DELTA_REGEX, expiration):
1956 amount = int(match.group(1))
1957 units = _EXPIRATION_CONVERSIONS.get(match.group(2).lower(), 1)
1958 delta += amount * units
1959 return delta
1967 _file_path_positive_re = re.compile(r'^[ 0-9a-zA-Z\._\+/@\$-]{1,256}$')
1970 _file_path_negative_1_re = re.compile(r'\.\.|^\./|\.$|/\./|^-|^_ah/|^/')
1973 _file_path_negative_2_re = re.compile(r'//|/$')
1977 _file_path_negative_3_re = re.compile(r'^ | $|/ | /')
1980 def ValidFilename(filename):
1981 """Determines if filename is valid.
1983 filename must be a valid pathname.
1984 - It must contain only letters, numbers, @, _, +, /, $, ., and -.
1985 - It must be less than 256 chars.
1986 - It must not contain "/./", "/../", or "//".
1987 - It must not end in "/".
1988 - All spaces must be in the middle of a directory or file name.
1990 Args:
1991 filename: The filename to validate.
1993 Returns:
1994 An error string if the filename is invalid. Returns '' if the filename
1995 is valid.
1997 if _file_path_positive_re.match(filename) is None:
1998 return 'Invalid character in filename: %s' % filename
1999 if _file_path_negative_1_re.search(filename) is not None:
2000 return ('Filename cannot contain "." or ".." '
2001 'or start with "-" or "_ah/": %s' %
2002 filename)
2003 if _file_path_negative_2_re.search(filename) is not None:
2004 return 'Filename cannot have trailing / or contain //: %s' % filename
2005 if _file_path_negative_3_re.search(filename) is not None:
2006 return 'Any spaces must be in the middle of a filename: %s' % filename
2007 return ''