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 """A library for managing flags-like configuration that update dynamically.
32 from google
.appengine
.api
import memcache
33 from google
.appengine
.ext
import db
34 from google
.appengine
.api
import validation
35 from google
.appengine
.api
import yaml_object
37 from google
.appengine
.api
import memcache
38 from google
.appengine
.ext
import db
39 from google
.appengine
.ext
import validation
40 from google
.appengine
.ext
import yaml_object
46 DATASTORE_DEADLINE
= 1.5
49 RESERVED_MARKER
= 'ah__conf__'
53 NAMESPACE
= '_' + RESERVED_MARKER
55 CONFIG_KIND
= '_AppEngine_Config'
57 ACTIVE_KEY_NAME
= 'active'
59 FILENAMES
= ['conf.yaml', 'conf.yml']
61 PARAMETERS
= 'parameters'
64 PARAMETER_NAME_REGEX
= '[a-zA-Z][a-zA-Z0-9_]*'
70 class Config(db
.Expando
):
71 """The representation of a config in the datastore and memcache."""
82 ah__conf__version
= db
.IntegerProperty(default
=0, required
=True)
86 """Override the kind name to prevent collisions with users."""
89 def ah__conf__load_from_yaml(self
, parsed_config
):
90 """Loads all the params from a YAMLConfiguration into expando fields.
92 We set these expando properties with a special name prefix 'p_' to
93 keep them separate from the static attributes of Config. That way we
94 don't have to check elsewhere to make sure the user doesn't stomp on
95 our built in properties.
98 parse_config: A YAMLConfiguration.
100 for key
, value
in parsed_config
.parameters
.iteritems():
101 setattr(self
, key
, value
)
104 class _ValidParameterName(validation
.Validator
):
105 """Validator to check if a value is a valid config parameter name.
107 We only allow valid python attribute names without leading underscores
108 that also do not collide with reserved words in the datastore models.
111 self
.regex
= validation
.Regex(PARAMETER_NAME_REGEX
)
113 def Validate(self
, value
, key
):
114 """Check that all parameter names are valid.
116 This is used as a validator when parsing conf.yaml.
119 value: the value to check.
120 key: A description of the context for which this value is being
126 value
= self
.regex
.Validate(value
, key
)
129 db
.check_reserved_word(value
)
130 except db
.ReservedWordError
:
131 raise validation
.ValidationError(
132 'The config parameter name %.100r is reserved by db.Model see: '
133 'https://developers.google.com/appengine/docs/python/datastore/'
134 'modelclass#Disallowed_Property_Names for details.' % value
)
136 if value
.startswith(RESERVED_MARKER
):
137 raise validation
.ValidationError(
138 'The config parameter name %.100r is reserved, as are all names '
139 'beginning with \'%s\', please choose a different name.' % (
140 value
, RESERVED_MARKER
))
145 class _Scalar(validation
.Validator
):
146 """Validator to check if a value is a simple scalar type.
148 We only allow scalars that are well supported by both the datastore and YAML.
150 ALLOWED_PARAMETER_VALUE_TYPES
= frozenset(
151 [bool, int, long, float, str, unicode])
153 def Validate(self
, value
, key
):
154 """Check that all parameters are scalar values.
156 This is used as a validator when parsing conf.yaml
159 value: the value to check.
160 key: the name of parameter corresponding to this value.
163 We just return value unchanged.
165 if type(value
) not in self
.ALLOWED_PARAMETER_VALUE_TYPES
:
166 raise validation
.ValidationError(
167 'Expected scalar value for parameter: %s, but found %.100r which '
168 'is type %s' % (key
, value
, type(value
).__name
__))
173 class _ParameterDict(validation
.ValidatedDict
):
174 """This class validates the parameters dictionary in YAMLConfiguration.
176 Keys must look like non-private python identifiers and values
177 must be a supported scalar. See the class comment for YAMLConfiguration.
179 KEY_VALIDATOR
= _ValidParameterName()
180 VALUE_VALIDATOR
= _Scalar()
183 class YAMLConfiguration(validation
.Validated
):
184 """This class describes the structure of a conf.yaml file.
186 At the top level the file should have a params attribue which is a mapping
187 from strings to scalars. For example:
190 background_color: 'red'
192 boolean_valued_param: true
194 ATTRIBUTES
= {PARAMETERS
: _ParameterDict
}
197 def LoadSingleConf(stream
):
198 """Load a conf.yaml file or string and return a YAMLConfiguration object.
201 stream: a file object corresponding to a conf.yaml file, or its contents
205 A YAMLConfiguration instance
207 return yaml_object
.BuildSingleObject(YAMLConfiguration
, stream
)
212 def _find_yaml_path():
213 """Traverse directory trees to find conf.yaml file.
215 Begins with the current working direcotry and then moves up the
216 directory structure until the file is found..
219 the path of conf.yaml file or None if not found.
221 current
, last
= os
.getcwd(), None
222 while current
!= last
:
223 for yaml_name
in FILENAMES
:
224 yaml_path
= os
.path
.join(current
, yaml_name
)
225 if os
.path
.exists(yaml_path
):
228 current
, last
= os
.path
.dirname(current
), current
233 def _fetch_from_local_file(pathfinder
=_find_yaml_path
, fileopener
=open):
234 """Get the configuration that was uploaded with this version.
237 pathfinder: a callable to use for finding the path of the conf.yaml
238 file. This is only for use in testing.
239 fileopener: a callable to use for opening a named file. This is
240 only for use in testing.
243 A config class instance for the options that were uploaded. If there
244 is no config file, return None
248 yaml_path
= pathfinder()
251 config
.ah__conf__load_from_yaml(LoadSingleConf(fileopener(yaml_path
)))
252 logging
.debug('Loaded conf parameters from conf.yaml.')
258 def _get_active_config_key(app_version
):
259 """Generate the key for the active config record belonging to app_version.
262 app_version: the major version you want configuration data for.
265 The key for the active Config record for the given app_version.
267 return db
.Key
.from_path(
269 '%s/%s' % (app_version
, ACTIVE_KEY_NAME
),
273 def _fetch_latest_from_datastore(app_version
):
274 """Get the latest configuration data for this app-version from the datastore.
277 app_version: the major version you want configuration data for.
280 We populate memcache with whatever we find in the datastore.
283 A config class instance for most recently set options or None if the
284 query could not complete due to a datastore exception.
291 rpc
= db
.create_rpc(deadline
=DATASTORE_DEADLINE
,
292 read_policy
=db
.EVENTUAL_CONSISTENCY
)
293 key
= _get_active_config_key(app_version
)
296 config
= Config
.get(key
, rpc
=rpc
)
297 logging
.debug('Loaded most recent conf data from datastore.')
299 logging
.warning('Tried but failed to fetch latest conf data from the '
303 memcache
.set(app_version
, db
.model_to_protobuf(config
).Encode(),
305 logging
.debug('Wrote most recent conf data into memcache.')
310 def _fetch_latest_from_memcache(app_version
):
311 """Get the latest configuration data for this app-version from memcache.
314 app_version: the major version you want configuration data for.
317 A Config class instance for most recently set options or None if none
318 could be found in memcache.
320 proto_string
= memcache
.get(app_version
, namespace
=NAMESPACE
)
322 logging
.debug('Loaded most recent conf data from memcache.')
323 return db
.model_from_protobuf(proto_string
)
325 logging
.debug('Tried to load conf data from memcache, but found nothing.')
329 def _inspect_environment():
330 """Return relevant information from the cgi environment.
332 This is mostly split out to simplify testing.
335 A tuple: (app_version, conf_version, development)
336 app_version: the major version of the current application.
337 conf_version: the current configuration version.
338 development: a boolean, True if we're running under devappserver.
340 app_version
= os
.environ
['CURRENT_VERSION_ID'].rsplit('.', 1)[0]
341 conf_version
= int(os
.environ
.get('CURRENT_CONFIGURATION_VERSION', '0'))
342 development
= os
.environ
.get('SERVER_SOFTWARE', '').startswith('Development/')
343 return (app_version
, conf_version
, development
)
347 """Update the local config cache from memcache/datastore.
349 Normally configuration parameters are only refreshed at the start of a
350 new request. If you have a very long running request, or you just need
351 the freshest data for some reason, you can call this function to force
354 app_version
, _
, _
= _inspect_environment()
359 global _cached_config
361 new_config
= _fetch_latest_from_memcache(app_version
)
364 new_config
= _fetch_latest_from_datastore(app_version
)
367 _cached_config
= new_config
371 """Test if this is the first call to this function in the current request.
373 This function will return True exactly once for each request
374 Subsequent calls in the same request will return False.
377 True if this is the first call in a given request, False otherwise.
379 if RESERVED_MARKER
in os
.environ
:
382 os
.environ
[RESERVED_MARKER
] = RESERVED_MARKER
387 """Check if the current cached config is stale, and if so update it."""
395 app_version
, current_config_version
, development
= _inspect_environment()
397 global _cached_config
399 if (development
and _new_request()) or not _cached_config
:
400 _cached_config
= _fetch_from_local_file() or Config()
402 if _cached_config
.ah__conf__version
< current_config_version
:
403 newconfig
= _fetch_latest_from_memcache(app_version
)
404 if not newconfig
or newconfig
.ah__conf__version
< current_config_version
:
405 newconfig
= _fetch_latest_from_datastore(app_version
)
409 _cached_config
= newconfig
or _cached_config
411 return _cached_config
414 def get(name
, default
=None):
415 """Get the value of a configuration parameter.
417 This function is guaranteed to return the same value for every call
418 during a single request.
421 name: The name of the configuration parameter you want a value for.
422 default: A default value to return if the named parameter doesn't exist.
425 The string value of the configuration parameter.
427 return getattr(_get_config(), name
, default
)
431 """Return an object with an attribute for each conf parameter.
434 An object with an attribute for each conf parameter.