| #!/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. |
| # |
| """A memcache viewer and editor UI.""" |
| |
| |
| import datetime |
| import pprint |
| import types |
| import urllib |
| |
| from google.appengine.tools.devappserver2.admin import admin_request_handler |
| |
| from google.appengine.api import memcache |
| |
| |
| def _to_bool(string_value): |
| """Convert string to boolean value. |
| |
| Args: |
| string_value: A string. |
| |
| Returns: |
| Boolean. True if string_value is "true", False if string_value is |
| "false". This is case-insensitive. |
| |
| Raises: |
| ValueError: string_value not "true" or "false". |
| """ |
| string_value_low = string_value.lower() |
| if string_value_low not in ('false', 'true'): |
| raise ValueError( |
| 'invalid literal for boolean: %s (must be "true" or "false")' % |
| string_value) |
| return string_value_low == 'true' |
| |
| |
| class MemcacheViewerRequestHandler(admin_request_handler.AdminRequestHandler): |
| # Supported types: type + conversion function + string description |
| # Order is important, must check Boolean before Integer. |
| TYPES = ((str, str, 'String'), |
| (unicode, unicode, 'Unicode String'), |
| (bool, _to_bool, 'Boolean'), |
| (int, int, 'Integer'), |
| (long, long, 'Long Integer'), |
| (float, float, 'Float')) |
| DEFAULT_TYPESTR_FOR_NEW = 'String' |
| |
| def _get_memcache_value_and_type(self, key): |
| """Fetch value from memcache and detect its type. |
| |
| Args: |
| key: String |
| |
| Returns: |
| (value, type), value is a Python object or None if the key was not set in |
| the cache, type is a string describing the type of the value. |
| """ |
| try: |
| value = memcache.get(key) |
| except (pickle.UnpicklingError, AttributeError, EOFError, ImportError, |
| IndexError), e: |
| # Pickled data could be broken or the user might have stored custom class |
| # instances in the cache (which can't be unpickled from here). |
| msg = 'Failed to retrieve value from cache: %s' % e |
| return msg, 'error' |
| |
| if value is None: |
| # No such value in the cache yet. |
| return None, self.DEFAULT_TYPESTR_FOR_NEW |
| |
| # Check if one of the editable types. |
| for typeobj, _, typestr in self.TYPES: |
| if isinstance(value, typeobj): |
| break |
| else: |
| # Unsupported type, just print nicely. |
| typestr = 'pickled' |
| value = pprint.pformat(value, indent=2) |
| |
| return value, typestr |
| |
| def _set_memcache_value(self, key, type_, value): |
| """Convert a string value and store the result in memcache. |
| |
| Args: |
| key: String |
| type_: String, describing what type the value should have in the cache. |
| value: String, will be converted according to type_. |
| |
| Returns: |
| Result of memcache.set(key, converted_value). True if value was set. |
| |
| Raises: |
| ValueError: Value can't be converted according to type_. |
| """ |
| for _, converter, typestr in self.TYPES: |
| if typestr == type_: |
| value = converter(value) |
| break |
| else: |
| raise ValueError('Type %s not supported.' % type_) |
| return memcache.set(key, value) |
| |
| def get(self): |
| """Show template and prepare stats and/or key+value to display/edit.""" |
| values = {'request': self.request, |
| 'message': self.request.get('message')} |
| |
| edit = self.request.get('edit') |
| key = self.request.get('key') |
| if edit: |
| # Show the form to edit/create the value. |
| key = edit |
| values['show_stats'] = False |
| values['show_value'] = False |
| values['show_valueform'] = True |
| values['types'] = [typestr for _, _, typestr in self.TYPES] |
| elif key: |
| # A key was given, show it's value on the stats page. |
| values['show_stats'] = True |
| values['show_value'] = True |
| values['show_valueform'] = False |
| else: |
| # Plain stats display + key lookup form. |
| values['show_stats'] = True |
| values['show_valueform'] = False |
| values['show_value'] = False |
| |
| if key: |
| values['key'] = key |
| values['value'], values['type'] = self._get_memcache_value_and_type(key) |
| values['key_exists'] = values['value'] is not None |
| |
| if values['type'] in ('pickled', 'error'): |
| values['writable'] = False |
| else: |
| values['writable'] = True |
| |
| if values['show_stats']: |
| memcache_stats = memcache.get_stats() |
| if not memcache_stats: |
| # No stats means no memcache usage. |
| memcache_stats = {'hits': 0, 'misses': 0, 'byte_hits': 0, 'items': 0, |
| 'bytes': 0, 'oldest_item_age': 0} |
| values['stats'] = memcache_stats |
| try: |
| hitratio = memcache_stats['hits'] * 100 / (memcache_stats['hits'] |
| + memcache_stats['misses']) |
| except ZeroDivisionError: |
| hitratio = 0 |
| values['hitratio'] = hitratio |
| # TODO: oldest_item_age should be formatted in a more useful |
| # way. |
| delta_t = datetime.timedelta(seconds=memcache_stats['oldest_item_age']) |
| values['oldest_item_age'] = datetime.datetime.now() - delta_t |
| |
| self.response.write(self.render('memcache_viewer.html', values)) |
| |
| def _urlencode(self, query): |
| """Encode a dictionary into a URL query string. |
| |
| In contrast to urllib this encodes unicode characters as UTF8. |
| |
| Args: |
| query: Dictionary of key/value pairs. |
| |
| Returns: |
| String. |
| """ |
| return '&'.join('%s=%s' % (urllib.quote_plus(k.encode('utf8')), |
| urllib.quote_plus(v.encode('utf8'))) |
| for k, v in query.iteritems()) |
| |
| def post(self): |
| """Handle modifying actions and/or redirect to GET page.""" |
| next_param = {} |
| |
| if self.request.get('action:flush'): |
| if memcache.flush_all(): |
| next_param['message'] = 'Cache flushed, all keys dropped.' |
| else: |
| next_param['message'] = 'Flushing the cache failed. Please try again.' |
| |
| elif self.request.get('action:display'): |
| next_param['key'] = self.request.get('key') |
| |
| elif self.request.get('action:edit'): |
| next_param['edit'] = self.request.get('key') |
| |
| elif self.request.get('action:delete'): |
| key = self.request.get('key') |
| result = memcache.delete(key) |
| if result == memcache.DELETE_NETWORK_FAILURE: |
| next_param['message'] = ('ERROR: Network failure, key "%s" not deleted.' |
| % key) |
| elif result == memcache.DELETE_ITEM_MISSING: |
| next_param['message'] = 'Key "%s" not in cache.' % key |
| elif result == memcache.DELETE_SUCCESSFUL: |
| next_param['message'] = 'Key "%s" deleted.' % key |
| else: |
| next_param['message'] = ('Unknown return value. Key "%s" might still ' |
| 'exist.' % key) |
| |
| elif self.request.get('action:save'): |
| key = self.request.get('key') |
| value = self.request.get('value') |
| type_ = self.request.get('type') |
| next_param['key'] = key |
| try: |
| if self._set_memcache_value(key, type_, value): |
| next_param['message'] = 'Key "%s" saved.' % key |
| else: |
| next_param['message'] = 'ERROR: Failed to save key "%s".' % key |
| except ValueError, e: |
| next_param['message'] = 'ERROR: Unable to encode value: %s' % e |
| |
| elif self.request.get('action:cancel'): |
| next_param['key'] = self.request.get('key') |
| |
| else: |
| next_param['message'] = 'Unknown action.' |
| |
| next = self.request.path_url |
| if next_param: |
| next = '%s?%s' % (next, self._urlencode(next_param)) |
| self.redirect(next) |