Index: appengine/monorail/framework/framework_helpers.py |
diff --git a/appengine/monorail/framework/framework_helpers.py b/appengine/monorail/framework/framework_helpers.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..2b30a63a70ee28a58bbe6e7f3bab9aba14551512 |
--- /dev/null |
+++ b/appengine/monorail/framework/framework_helpers.py |
@@ -0,0 +1,671 @@ |
+# Copyright 2016 The Chromium Authors. All rights reserved. |
+# Use of this source code is govered by a BSD-style |
+# license that can be found in the LICENSE file or at |
+# https://developers.google.com/open-source/licenses/bsd |
+ |
+"""Helper functions and classes used throughout Monorail.""" |
+ |
+import logging |
+import random |
+import string |
+import textwrap |
+import threading |
+import time |
+import traceback |
+import urllib |
+import urlparse |
+ |
+from google.appengine.api import app_identity |
+ |
+from third_party import ezt |
+ |
+import settings |
+from framework import actionlimit |
+from framework import framework_constants |
+from framework import template_helpers |
+from framework import timestr |
+from framework import urls |
+from services import client_config_svc |
+ |
+ |
+# For random key generation |
+RANDOM_KEY_LENGTH = 128 |
+RANDOM_KEY_CHARACTERS = string.ascii_letters + string.digits |
+ |
+# params recognized by FormatURL, in the order they will appear in the url |
+RECOGNIZED_PARAMS = ['can', 'start', 'num', 'q', 'colspec', 'groupby', 'sort', |
+ 'show', 'format', 'me', 'table_title', 'projects'] |
+ |
+ |
+def retry(tries, delay=1, backoff=2): |
+ """A retry decorator with exponential backoff. |
+ |
+ Functions are retried when Exceptions occur. |
+ |
+ Args: |
+ tries: int Number of times to retry, set to 0 to disable retry. |
+ delay: float Initial sleep time in seconds. |
+ backoff: float Must be greater than 1, further failures would sleep |
+ delay*=backoff seconds. |
+ """ |
+ if backoff <= 1: |
+ raise ValueError("backoff must be greater than 1") |
+ if tries < 0: |
+ raise ValueError("tries must be 0 or greater") |
+ if delay <= 0: |
+ raise ValueError("delay must be greater than 0") |
+ |
+ def decorator(func): |
+ def wrapper(*args, **kwargs): |
+ _tries, _delay = tries, delay |
+ _tries += 1 # Ensure we call func at least once. |
+ while _tries > 0: |
+ try: |
+ ret = func(*args, **kwargs) |
+ return ret |
+ except Exception: |
+ _tries -= 1 |
+ if _tries == 0: |
+ logging.error('Exceeded maximum number of retries for %s.', |
+ func.__name__) |
+ raise |
+ trace_str = traceback.format_exc() |
+ logging.warning('Retrying %s due to Exception: %s', |
+ func.__name__, trace_str) |
+ time.sleep(_delay) |
+ _delay *= backoff # Wait longer the next time we fail. |
+ return wrapper |
+ return decorator |
+ |
+ |
+class PromiseCallback(object): |
+ """Executes the work of a Promise and then dereferences everything.""" |
+ |
+ def __init__(self, promise, callback, *args, **kwargs): |
+ self.promise = promise |
+ self.callback = callback |
+ self.args = args |
+ self.kwargs = kwargs |
+ |
+ def __call__(self): |
+ try: |
+ self.promise._WorkOnPromise(self.callback, *self.args, **self.kwargs) |
+ finally: |
+ # Make sure we no longer hold onto references to anything. |
+ self.promise = self.callback = self.args = self.kwargs = None |
+ |
+ |
+class Promise(object): |
+ """Class for promises to deliver a value in the future. |
+ |
+ A thread is started to run callback(args), that thread |
+ should return the value that it generates, or raise an expception. |
+ p.WaitAndGetValue() will block until a value is available. |
+ If an exception was raised, p.WaitAndGetValue() will re-raise the |
+ same exception. |
+ """ |
+ |
+ def __init__(self, callback, *args, **kwargs): |
+ """Initialize the promise and immediately call the supplied function. |
+ |
+ Args: |
+ callback: Function that takes the args and returns the promise value. |
+ *args: Any arguments to the target function. |
+ **kwargs: Any keyword args for the target function. |
+ """ |
+ |
+ self.has_value = False |
+ self.value = None |
+ self.event = threading.Event() |
+ self.exception = None |
+ |
+ promise_callback = PromiseCallback(self, callback, *args, **kwargs) |
+ |
+ # Execute the callback in another thread. |
+ promise_thread = threading.Thread(target=promise_callback) |
+ promise_thread.start() |
+ |
+ def _WorkOnPromise(self, callback, *args, **kwargs): |
+ """Run callback to compute the promised value. Save any exceptions.""" |
+ try: |
+ self.value = callback(*args, **kwargs) |
+ except Exception as e: |
+ trace_str = traceback.format_exc() |
+ logging.info('Exception while working on promise: %s\n', trace_str) |
+ # Add the stack trace at this point to the exception. That way, in the |
+ # logs, we can see what happened further up in the call stack |
+ # than WaitAndGetValue(), which re-raises exceptions. |
+ e.pre_promise_trace = trace_str |
+ self.exception = e |
+ finally: |
+ self.has_value = True |
+ self.event.set() |
+ |
+ def WaitAndGetValue(self): |
+ """Block until my value is available, then return it or raise exception.""" |
+ self.event.wait() |
+ if self.exception: |
+ raise self.exception # pylint: disable=raising-bad-type |
+ return self.value |
+ |
+ |
+def FormatAbsoluteURLForDomain( |
+ host, project_name, servlet_name, scheme='https', **kwargs): |
+ """A variant of FormatAbsoluteURL for when request objects are not available. |
+ |
+ Args: |
+ host: string with hostname and optional port, e.g. 'localhost:8080'. |
+ project_name: the destination project name, if any. |
+ servlet_name: site or project-local url fragement of dest page. |
+ scheme: url scheme, e.g., 'http' or 'https'. |
+ **kwargs: additional query string parameters may be specified as named |
+ arguments to this function. |
+ |
+ Returns: |
+ A full url beginning with 'http[s]://'. |
+ """ |
+ path_and_args = FormatURL(None, servlet_name, **kwargs) |
+ |
+ if host: |
+ domain_port = host.split(':') |
+ domain_port[0] = GetPreferredDomain(domain_port[0]) |
+ host = ':'.join(domain_port) |
+ |
+ absolute_domain_url = '%s://%s' % (scheme, host) |
+ if project_name: |
+ return '%s/p/%s%s' % (absolute_domain_url, project_name, path_and_args) |
+ return absolute_domain_url + path_and_args |
+ |
+ |
+def FormatAbsoluteURL( |
+ mr, servlet_name, include_project=True, project_name=None, |
+ scheme=None, copy_params=True, **kwargs): |
+ """Return an absolute URL to a servlet with old and new params. |
+ |
+ Args: |
+ mr: info parsed from the current request. |
+ servlet_name: site or project-local url fragement of dest page. |
+ include_project: if True, include the project home url as part of the |
+ destination URL (as long as it is specified either in mr |
+ or as the project_name param.) |
+ project_name: the destination project name, to override |
+ mr.project_name if include_project is True. |
+ scheme: either 'http' or 'https', to override mr.request.scheme. |
+ copy_params: if True, copy well-known parameters from the existing request. |
+ **kwargs: additional query string parameters may be specified as named |
+ arguments to this function. |
+ |
+ Returns: |
+ A full url beginning with 'http[s]://'. The destination URL will be in |
+ the same domain as the current request. |
+ """ |
+ path_and_args = FormatURL( |
+ mr if copy_params else None, servlet_name, **kwargs) |
+ scheme = scheme or mr.request.scheme |
+ |
+ project_base = '' |
+ if include_project: |
+ project_base = '/p/%s' % (project_name or mr.project_name) |
+ |
+ return '%s://%s%s%s' % (scheme, mr.request.host, project_base, path_and_args) |
+ |
+ |
+def FormatMovedProjectURL(mr, moved_to): |
+ """Return a transformation of the given url into the given project. |
+ |
+ Args: |
+ mr: common information parsed from the HTTP request. |
+ moved_to: A string from a project's moved_to field that matches |
+ framework_bizobj.RE_PROJECT_NAME. |
+ |
+ Returns: |
+ The url transposed into the given destination project. |
+ """ |
+ project_name = moved_to |
+ _, _, path, parameters, query, fragment_identifier = urlparse.urlparse( |
+ mr.current_page_url) |
+ # Strip off leading "/p/<moved from project>" |
+ path = '/' + path.split('/', 3)[3] |
+ rest_of_url = urlparse.urlunparse( |
+ ('', '', path, parameters, query, fragment_identifier)) |
+ return '/p/%s%s' % (project_name, rest_of_url) |
+ |
+ |
+def FormatURL(mr, servlet_path, **kwargs): |
+ """Return a project relative URL to a servlet with old and new params.""" |
+ # Standard params not overridden in **kwargs come first, followed by kwargs. |
+ # The exception is the 'id' param. If present then the 'id' param always comes |
+ # first. See bugs.chromium.org/p/monorail/issues/detail?id=374 |
+ all_params = [] |
+ if kwargs.get('id'): |
+ all_params.append(('id', kwargs['id'])) |
+ if mr: |
+ all_params.extend( |
+ (name, mr.GetParam(name)) for name in RECOGNIZED_PARAMS |
+ if name not in kwargs) |
+ |
+ all_params.extend( |
+ # Ignore the 'id' param since we already added it above. |
+ sorted([kwarg for kwarg in kwargs.items() if kwarg[0] != 'id'])) |
+ return _FormatQueryString(servlet_path, all_params) |
+ |
+ |
+def _FormatQueryString(url, params): |
+ """URLencode a list of parameters and attach them to the end of a URL.""" |
+ param_string = '&'.join( |
+ '%s=%s' % (name, urllib.quote(unicode(value).encode('utf-8'))) |
+ for name, value in params if value is not None) |
+ if not param_string: |
+ qs_start_char = '' |
+ elif '?' in url: |
+ qs_start_char = '&' |
+ else: |
+ qs_start_char = '?' |
+ return '%s%s%s' % (url, qs_start_char, param_string) |
+ |
+ |
+def WordWrapSuperLongLines(s, max_cols=100): |
+ """Reformat input that was not word-wrapped by the browser. |
+ |
+ Args: |
+ s: the string to be word-wrapped, it may have embedded newlines. |
+ max_cols: int maximum line length. |
+ |
+ Returns: |
+ Wrapped text string. |
+ |
+ Rather than wrap the whole thing, we only wrap super-long lines and keep |
+ all the reasonable lines formated as-is. |
+ """ |
+ lines = [textwrap.fill(line, max_cols) for line in s.splitlines()] |
+ wrapped_text = '\n'.join(lines) |
+ |
+ # The split/join logic above can lose one final blank line. |
+ if s.endswith('\n') or s.endswith('\r'): |
+ wrapped_text += '\n' |
+ |
+ return wrapped_text |
+ |
+ |
+def StaticCacheHeaders(): |
+ """Returns HTTP headers for static content, based on the current time.""" |
+ year_from_now = int(time.time()) + framework_constants.SECS_PER_YEAR |
+ headers = [ |
+ ('Cache-Control', |
+ 'max-age=%d, private' % framework_constants.SECS_PER_YEAR), |
+ ('Last-Modified', timestr.TimeForHTMLHeader()), |
+ ('Expires', timestr.TimeForHTMLHeader(when=year_from_now)), |
+ ] |
+ logging.info('static headers are %r', headers) |
+ return headers |
+ |
+ |
+def ComputeListDeltas(old_list, new_list): |
+ """Given an old and new list, return the items added and removed. |
+ |
+ Args: |
+ old_list: old list of values for comparison. |
+ new_list: new list of values for comparison. |
+ |
+ Returns: |
+ Two lists: one with all the values added (in new_list but was not |
+ in old_list), and one with all the values removed (not in new_list |
+ but was in old_lit). |
+ """ |
+ if old_list == new_list: |
+ return [], [] # A common case: nothing was added or removed. |
+ |
+ added = set(new_list) |
+ added.difference_update(old_list) |
+ removed = set(old_list) |
+ removed.difference_update(new_list) |
+ return list(added), list(removed) |
+ |
+ |
+def GetRoleName(effective_ids, project): |
+ """Determines the name of the role a member has for a given project. |
+ |
+ Args: |
+ effective_ids: set of user IDs to get the role name for. |
+ project: Project PB containing the different the different member lists. |
+ |
+ Returns: |
+ The name of the role. |
+ """ |
+ if not effective_ids.isdisjoint(project.owner_ids): |
+ return 'Owner' |
+ if not effective_ids.isdisjoint(project.committer_ids): |
+ return 'Committer' |
+ if not effective_ids.isdisjoint(project.contributor_ids): |
+ return 'Contributor' |
+ return None |
+ |
+ |
+class UserSettings(object): |
+ """Abstract class providing static methods for user settings forms.""" |
+ |
+ @classmethod |
+ def GatherUnifiedSettingsPageData( |
+ cls, logged_in_user_id, settings_user_view, settings_user): |
+ """Gather EZT variables needed for the unified user settings form. |
+ |
+ Args: |
+ logged_in_user_id: The user ID of the acting user. |
+ settings_user_view: The UserView of the target user. |
+ settings_user: The User PB of the target user. |
+ |
+ Returns: |
+ A dictionary giving the names and values of all the variables to |
+ be exported to EZT to support the unified user settings form template. |
+ """ |
+ |
+ def ActionLastReset(action_limit): |
+ """Return a formatted time string for the last action limit reset.""" |
+ if action_limit: |
+ return time.asctime(time.localtime(action_limit.reset_timestamp)) |
+ return 'Never' |
+ |
+ def DefaultLifetimeLimit(action_type): |
+ """Return the deault lifetime limit for the give type of action.""" |
+ return actionlimit.ACTION_LIMITS[action_type][3] |
+ |
+ def DefaultPeriodSoftLimit(action_type): |
+ """Return the deault period soft limit for the give type of action.""" |
+ return actionlimit.ACTION_LIMITS[action_type][1] |
+ |
+ def DefaultPeriodHardLimit(action_type): |
+ """Return the deault period jard limit for the give type of action.""" |
+ return actionlimit.ACTION_LIMITS[action_type][2] |
+ |
+ project_creation_lifetime_limit = ( |
+ (settings_user.project_creation_limit and |
+ settings_user.project_creation_limit.lifetime_limit) or |
+ DefaultLifetimeLimit(actionlimit.PROJECT_CREATION)) |
+ project_creation_soft_limit = ( |
+ (settings_user.project_creation_limit and |
+ settings_user.project_creation_limit.period_soft_limit) or |
+ DefaultPeriodSoftLimit(actionlimit.PROJECT_CREATION)) |
+ project_creation_hard_limit = ( |
+ (settings_user.project_creation_limit and |
+ settings_user.project_creation_limit.period_hard_limit) or |
+ DefaultPeriodHardLimit(actionlimit.PROJECT_CREATION)) |
+ issue_comment_lifetime_limit = ( |
+ (settings_user.issue_comment_limit and |
+ settings_user.issue_comment_limit.lifetime_limit) or |
+ DefaultLifetimeLimit(actionlimit.ISSUE_COMMENT)) |
+ issue_comment_soft_limit = ( |
+ (settings_user.issue_comment_limit and |
+ settings_user.issue_comment_limit.period_soft_limit) or |
+ DefaultPeriodSoftLimit(actionlimit.ISSUE_COMMENT)) |
+ issue_comment_hard_limit = ( |
+ (settings_user.issue_comment_limit and |
+ settings_user.issue_comment_limit.period_hard_limit) or |
+ DefaultPeriodHardLimit(actionlimit.ISSUE_COMMENT )) |
+ issue_attachment_lifetime_limit = ( |
+ (settings_user.issue_attachment_limit and |
+ settings_user.issue_attachment_limit.lifetime_limit) or |
+ DefaultLifetimeLimit(actionlimit.ISSUE_ATTACHMENT)) |
+ issue_attachment_soft_limit = ( |
+ (settings_user.issue_attachment_limit and |
+ settings_user.issue_attachment_limit.period_soft_limit) or |
+ DefaultPeriodSoftLimit(actionlimit.ISSUE_ATTACHMENT)) |
+ issue_attachment_hard_limit = ( |
+ (settings_user.issue_attachment_limit and |
+ settings_user.issue_attachment_limit.period_hard_limit) or |
+ DefaultPeriodHardLimit(actionlimit.ISSUE_ATTACHMENT)) |
+ issue_bulk_edit_lifetime_limit = ( |
+ (settings_user.issue_bulk_edit_limit and |
+ settings_user.issue_bulk_edit_limit.lifetime_limit) or |
+ DefaultLifetimeLimit(actionlimit.ISSUE_BULK_EDIT)) |
+ issue_bulk_edit_soft_limit = ( |
+ (settings_user.issue_bulk_edit_limit and |
+ settings_user.issue_bulk_edit_limit.period_soft_limit) or |
+ DefaultPeriodSoftLimit(actionlimit.ISSUE_BULK_EDIT)) |
+ issue_bulk_edit_hard_limit = ( |
+ (settings_user.issue_bulk_edit_limit and |
+ settings_user.issue_bulk_edit_limit.period_hard_limit) or |
+ DefaultPeriodHardLimit(actionlimit.ISSUE_BULK_EDIT)) |
+ api_request_lifetime_limit = ( |
+ (settings_user.api_request_limit and |
+ settings_user.api_request_limit.lifetime_limit) or |
+ DefaultLifetimeLimit(actionlimit.API_REQUEST)) |
+ api_request_soft_limit = ( |
+ (settings_user.api_request_limit and |
+ settings_user.api_request_limit.period_soft_limit) or |
+ DefaultPeriodSoftLimit(actionlimit.API_REQUEST)) |
+ api_request_hard_limit = ( |
+ (settings_user.api_request_limit and |
+ settings_user.api_request_limit.period_hard_limit) or |
+ DefaultPeriodHardLimit(actionlimit.API_REQUEST)) |
+ |
+ return { |
+ 'settings_user': settings_user_view, |
+ 'settings_user_pb': template_helpers.PBProxy(settings_user), |
+ 'settings_user_is_banned': ezt.boolean(settings_user.banned), |
+ 'settings_user_ignore_action_limits': ( |
+ ezt.boolean(settings_user.ignore_action_limits)), |
+ 'self': ezt.boolean(logged_in_user_id == settings_user_view.user_id), |
+ 'project_creation_reset': ( |
+ ActionLastReset(settings_user.project_creation_limit)), |
+ 'issue_comment_reset': ( |
+ ActionLastReset(settings_user.issue_comment_limit)), |
+ 'issue_attachment_reset': ( |
+ ActionLastReset(settings_user.issue_attachment_limit)), |
+ 'issue_bulk_edit_reset': ( |
+ ActionLastReset(settings_user.issue_bulk_edit_limit)), |
+ 'api_request_reset': ( |
+ ActionLastReset(settings_user.api_request_limit)), |
+ 'project_creation_lifetime_limit': project_creation_lifetime_limit, |
+ 'project_creation_soft_limit': project_creation_soft_limit, |
+ 'project_creation_hard_limit': project_creation_hard_limit, |
+ 'issue_comment_lifetime_limit': issue_comment_lifetime_limit, |
+ 'issue_comment_soft_limit': issue_comment_soft_limit, |
+ 'issue_comment_hard_limit': issue_comment_hard_limit, |
+ 'issue_attachment_lifetime_limit': issue_attachment_lifetime_limit, |
+ 'issue_attachment_soft_limit': issue_attachment_soft_limit, |
+ 'issue_attachment_hard_limit': issue_attachment_hard_limit, |
+ 'issue_bulk_edit_lifetime_limit': issue_bulk_edit_lifetime_limit, |
+ 'issue_bulk_edit_soft_limit': issue_bulk_edit_soft_limit, |
+ 'issue_bulk_edit_hard_limit': issue_bulk_edit_hard_limit, |
+ 'api_request_lifetime_limit': api_request_lifetime_limit, |
+ 'api_request_soft_limit': api_request_soft_limit, |
+ 'api_request_hard_limit': api_request_hard_limit, |
+ 'profile_url_fragment': ( |
+ settings_user_view.profile_url[len('/u/'):]), |
+ 'preview_on_hover': ezt.boolean(settings_user.preview_on_hover), |
+ } |
+ |
+ @classmethod |
+ def ProcessSettingsForm( |
+ cls, cnxn, user_service, post_data, user_id, user, admin=False): |
+ """Process the posted form data from the unified user settings form. |
+ |
+ Args: |
+ cnxn: connection to the SQL database. |
+ user_service: An instance of UserService for saving changes. |
+ post_data: The parsed post data from the form submission request. |
+ user_id: The user id of the target user. |
+ user: The user PB of the target user. |
+ admin: Whether settings reserved for admins are supported. |
+ """ |
+ obscure_email = 'obscure_email' in post_data |
+ |
+ kwargs = {} |
+ if admin: |
+ kwargs.update(is_site_admin='site_admin' in post_data, |
+ ignore_action_limits='ignore_action_limits' in post_data) |
+ kwargs.update(is_banned='banned' in post_data, |
+ banned_reason=post_data.get('banned_reason', '')) |
+ |
+ # action limits |
+ action_limit_updates = {} |
+ for action_name in actionlimit.ACTION_TYPE_NAMES.iterkeys(): |
+ reset_input = 'reset_' + action_name |
+ lifetime_input = action_name + '_lifetime_limit' |
+ soft_input = action_name + '_soft_limit' |
+ hard_input = action_name + '_hard_limit' |
+ pb_getter = action_name + '_limit' |
+ old_lifetime_limit = getattr(user, pb_getter).lifetime_limit |
+ old_soft_limit = getattr(user, pb_getter).period_soft_limit |
+ old_hard_limit = getattr(user, pb_getter).period_hard_limit |
+ |
+ # Try and get the new limit from post data. |
+ # If the user doesn't use an integer, act as if no change requested. |
+ def _GetLimit(post_data, limit_input, old_limit): |
+ try: |
+ new_limit = int(post_data[limit_input]) |
+ except (KeyError, ValueError): |
+ new_limit = old_limit |
+ return new_limit |
+ |
+ new_lifetime_limit = _GetLimit(post_data, lifetime_input, |
+ old_lifetime_limit) |
+ new_soft_limit = _GetLimit(post_data, soft_input, |
+ old_soft_limit) |
+ new_hard_limit = _GetLimit(post_data, hard_input, |
+ old_hard_limit) |
+ |
+ if ((new_lifetime_limit >= 0 and |
+ new_lifetime_limit != old_lifetime_limit) or |
+ (new_soft_limit >= 0 and new_soft_limit != old_soft_limit) or |
+ (new_hard_limit >= 0 and new_hard_limit != old_hard_limit)): |
+ action_limit_updates[action_name] = ( |
+ new_soft_limit, new_hard_limit, new_lifetime_limit) |
+ elif reset_input in post_data: |
+ action_limit_updates[action_name] = None |
+ kwargs.update(action_limit_updates=action_limit_updates) |
+ |
+ user_service.UpdateUserSettings( |
+ cnxn, user_id, user, notify='notify' in post_data, |
+ notify_starred='notify_starred' in post_data, |
+ preview_on_hover='preview_on_hover' in post_data, |
+ obscure_email=obscure_email, **kwargs) |
+ |
+ |
+def GetHostPort(): |
+ """Get string domain name and port number.""" |
+ |
+ app_id = app_identity.get_application_id() |
+ if ':' in app_id: |
+ domain, app_id = app_id.split(':') |
+ else: |
+ domain = '' |
+ |
+ if domain.startswith('google'): |
+ hostport = '%s.googleplex.com' % app_id |
+ else: |
+ hostport = '%s.appspot.com' % app_id |
+ |
+ return GetPreferredDomain(hostport) |
+ |
+ |
+def IssueCommentURL(hostport, project, local_id, seq_num=None): |
+ """Return a URL pointing directly to the specified comment.""" |
+ detail_url = FormatAbsoluteURLForDomain( |
+ hostport, project.project_name, urls.ISSUE_DETAIL, id=local_id) |
+ if seq_num: |
+ detail_url += '#c%d' % seq_num |
+ |
+ return detail_url |
+ |
+ |
+def MurmurHash3_x86_32(key, seed=0x0): |
+ """Implements the x86/32-bit version of Murmur Hash 3.0. |
+ |
+ MurmurHash3 is written by Austin Appleby, and is placed in the public |
+ domain. See https://code.google.com/p/smhasher/ for details. |
+ |
+ This pure python implementation of the x86/32 bit version of MurmurHash3 is |
+ written by Fredrik Kihlander and also placed in the public domain. |
+ See https://github.com/wc-duck/pymmh3 for details. |
+ |
+ The MurmurHash3 algorithm is chosen for these reasons: |
+ * It is fast, even when implemented in pure python. |
+ * It is remarkably well distributed, and unlikely to cause collisions. |
+ * It is stable and unchanging (any improvements will be in MurmurHash4). |
+ * It is well-tested, and easily usable in other contexts (such as bulk |
+ data imports). |
+ |
+ Args: |
+ key (string): the data that you want hashed |
+ seed (int): An offset, treated as essentially part of the key. |
+ |
+ Returns: |
+ A 32-bit integer (can be interpreted as either signed or unsigned). |
+ """ |
+ key = bytearray(key.encode('utf-8')) |
+ |
+ def fmix(h): |
+ h ^= h >> 16 |
+ h = (h * 0x85ebca6b) & 0xFFFFFFFF |
+ h ^= h >> 13 |
+ h = (h * 0xc2b2ae35) & 0xFFFFFFFF |
+ h ^= h >> 16 |
+ return h; |
+ |
+ length = len(key) |
+ nblocks = int(length / 4) |
+ |
+ h1 = seed; |
+ |
+ c1 = 0xcc9e2d51 |
+ c2 = 0x1b873593 |
+ |
+ # body |
+ for block_start in xrange(0, nblocks * 4, 4): |
+ k1 = key[ block_start + 3 ] << 24 | \ |
+ key[ block_start + 2 ] << 16 | \ |
+ key[ block_start + 1 ] << 8 | \ |
+ key[ block_start + 0 ] |
+ |
+ k1 = c1 * k1 & 0xFFFFFFFF |
+ k1 = (k1 << 15 | k1 >> 17) & 0xFFFFFFFF |
+ k1 = (c2 * k1) & 0xFFFFFFFF; |
+ |
+ h1 ^= k1 |
+ h1 = ( h1 << 13 | h1 >> 19 ) & 0xFFFFFFFF |
+ h1 = ( h1 * 5 + 0xe6546b64 ) & 0xFFFFFFFF |
+ |
+ # tail |
+ tail_index = nblocks * 4 |
+ k1 = 0 |
+ tail_size = length & 3 |
+ |
+ if tail_size >= 3: |
+ k1 ^= key[ tail_index + 2 ] << 16 |
+ if tail_size >= 2: |
+ k1 ^= key[ tail_index + 1 ] << 8 |
+ if tail_size >= 1: |
+ k1 ^= key[ tail_index + 0 ] |
+ |
+ if tail_size != 0: |
+ k1 = ( k1 * c1 ) & 0xFFFFFFFF |
+ k1 = ( k1 << 15 | k1 >> 17 ) & 0xFFFFFFFF |
+ k1 = ( k1 * c2 ) & 0xFFFFFFFF |
+ h1 ^= k1 |
+ |
+ return fmix( h1 ^ length ) |
+ |
+ |
+def MakeRandomKey(length=RANDOM_KEY_LENGTH, chars=RANDOM_KEY_CHARACTERS): |
+ """Return a string with lots of random characters.""" |
+ chars = [random.choice(chars) for _ in range(length)] |
+ return ''.join(chars) |
+ |
+ |
+def IsServiceAccount(email): |
+ """Return a boolean value whether this email is a service account.""" |
+ if email.endswith('gserviceaccount.com'): |
+ return True |
+ _, client_emails = ( |
+ client_config_svc.GetClientConfigSvc().GetClientIDEmails()) |
+ return email in client_emails |
+ |
+ |
+def GetPreferredDomain(domain): |
+ """Get preferred domain to display. |
+ |
+ The preferred domain replaces app_id for default version of monorail-prod |
+ and monorail-staging. |
+ """ |
+ return settings.preferred_domains.get(domain, domain) |