Merge branch 'main' into avm99963-monorail

Merged commit 34d8229ae2b51fb1a15bd208e6fe6185c94f6266

GitOrigin-RevId: 7ee0917f93a577e475f8e09526dd144d245593f4
diff --git a/third_party/endpoints/util.py b/third_party/endpoints/util.py
new file mode 100644
index 0000000..fe883d0
--- /dev/null
+++ b/third_party/endpoints/util.py
@@ -0,0 +1,300 @@
+# Copyright 2016 Google Inc. All Rights Reserved.
+#
+# 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.
+
+"""Helper utilities for the endpoints package."""
+
+# pylint: disable=g-bad-name
+from __future__ import absolute_import
+
+from six.moves import cStringIO
+import json
+import os
+import wsgiref.headers
+
+from google.appengine.api import app_identity
+from google.appengine.api.modules import modules
+
+
+class StartResponseProxy(object):
+  """Proxy for the typical WSGI start_response object."""
+
+  def __init__(self):
+    self.call_context = {}
+    self.body_buffer = cStringIO.StringIO()
+
+  def __enter__(self):
+    return self
+
+  def __exit__(self, exc_type, exc_value, traceback):
+    # Close out the cStringIO.StringIO buffer to prevent memory leakage.
+    if self.body_buffer:
+      self.body_buffer.close()
+
+  def Proxy(self, status, headers, exc_info=None):
+    """Save args, defer start_response until response body is parsed.
+
+    Create output buffer for body to be written into.
+    Note: this is not quite WSGI compliant: The body should come back as an
+      iterator returned from calling service_app() but instead, StartResponse
+      returns a writer that will be later called to output the body.
+    See google/appengine/ext/webapp/__init__.py::Response.wsgi_write()
+        write = start_response('%d %s' % self.__status, self.__wsgi_headers)
+        write(body)
+
+    Args:
+      status: Http status to be sent with this response
+      headers: Http headers to be sent with this response
+      exc_info: Exception info to be displayed for this response
+    Returns:
+      callable that takes as an argument the body content
+    """
+    self.call_context['status'] = status
+    self.call_context['headers'] = headers
+    self.call_context['exc_info'] = exc_info
+
+    return self.body_buffer.write
+
+  @property
+  def response_body(self):
+    return self.body_buffer.getvalue()
+
+  @property
+  def response_headers(self):
+    return self.call_context.get('headers')
+
+  @property
+  def response_status(self):
+    return self.call_context.get('status')
+
+  @property
+  def response_exc_info(self):
+    return self.call_context.get('exc_info')
+
+
+def send_wsgi_not_found_response(start_response, cors_handler=None):
+  return send_wsgi_response('404 Not Found', [('Content-Type', 'text/plain')],
+                            'Not Found', start_response,
+                            cors_handler=cors_handler)
+
+
+def send_wsgi_error_response(message, start_response, cors_handler=None):
+  body = json.dumps({'error': {'message': message}})
+  return send_wsgi_response('500', [('Content-Type', 'application/json')], body,
+                            start_response, cors_handler=cors_handler)
+
+
+def send_wsgi_rejected_response(rejection_error, start_response,
+                                cors_handler=None):
+  body = rejection_error.to_json()
+  return send_wsgi_response('400', [('Content-Type', 'application/json')], body,
+                            start_response, cors_handler=cors_handler)
+
+
+def send_wsgi_redirect_response(redirect_location, start_response,
+                                cors_handler=None):
+  return send_wsgi_response('302', [('Location', redirect_location)], '',
+                            start_response, cors_handler=cors_handler)
+
+
+def send_wsgi_no_content_response(start_response, cors_handler=None):
+  return send_wsgi_response('204 No Content', [], '', start_response,
+                            cors_handler)
+
+
+def send_wsgi_response(status, headers, content, start_response,
+                       cors_handler=None):
+  """Dump reformatted response to CGI start_response.
+
+  This calls start_response and returns the response body.
+
+  Args:
+    status: A string containing the HTTP status code to send.
+    headers: A list of (header, value) tuples, the headers to send in the
+      response.
+    content: A string containing the body content to write.
+    start_response: A function with semantics defined in PEP-333.
+    cors_handler: A handler to process CORS request headers and update the
+      headers in the response.  Or this can be None, to bypass CORS checks.
+
+  Returns:
+    A string containing the response body.
+  """
+  if cors_handler:
+    cors_handler.update_headers(headers)
+
+  # Update content length.
+  content_len = len(content) if content else 0
+  headers = [(header, value) for header, value in headers
+             if header.lower() != 'content-length']
+  headers.append(('Content-Length', '%s' % content_len))
+
+  start_response(status, headers)
+  return content
+
+
+def get_headers_from_environ(environ):
+  """Get a wsgiref.headers.Headers object with headers from the environment.
+
+  Headers in environ are prefixed with 'HTTP_', are all uppercase, and have
+  had dashes replaced with underscores.  This strips the HTTP_ prefix and
+  changes underscores back to dashes before adding them to the returned set
+  of headers.
+
+  Args:
+    environ: An environ dict for the request as defined in PEP-333.
+
+  Returns:
+    A wsgiref.headers.Headers object that's been filled in with any HTTP
+    headers found in environ.
+  """
+  headers = wsgiref.headers.Headers([])
+  for header, value in environ.items():
+    if header.startswith('HTTP_'):
+      headers[header[5:].replace('_', '-')] = value
+  # Content-Type is special; it does not start with 'HTTP_'.
+  if 'CONTENT_TYPE' in environ:
+    headers['CONTENT-TYPE'] = environ['CONTENT_TYPE']
+  return headers
+
+
+def put_headers_in_environ(headers, environ):
+  """Given a list of headers, put them into environ based on PEP-333.
+
+  This converts headers to uppercase, prefixes them with 'HTTP_', and
+  converts dashes to underscores before adding them to the environ dict.
+
+  Args:
+    headers: A list of (header, value) tuples.  The HTTP headers to add to the
+      environment.
+    environ: An environ dict for the request as defined in PEP-333.
+  """
+  for key, value in headers:
+    environ['HTTP_%s' % key.upper().replace('-', '_')] = value
+
+
+def is_running_on_app_engine():
+  return os.environ.get('GAE_MODULE_NAME') is not None
+
+
+def is_running_on_devserver():
+  server_software = os.environ.get('SERVER_SOFTWARE', '')
+  return (server_software.startswith('Development/') and
+    server_software != 'Development/1.0 (testbed)')
+
+
+def is_running_on_localhost():
+  return os.environ.get('SERVER_NAME') == 'localhost'
+
+
+def get_hostname_prefix():
+  """Returns the hostname prefix of a running Endpoints service.
+
+  The prefix is the portion of the hostname that comes before the API name.
+  For example, if a non-default version and a non-default service are in use,
+  the returned result would be '{VERSION}-dot-{SERVICE}-'.
+
+  Returns:
+    str, the hostname prefix.
+  """
+  parts = []
+
+  # Check if this is the default version
+  version = modules.get_current_version_name()
+  default_version = modules.get_default_version()
+  if version != default_version:
+    parts.append(version)
+
+  # Check if this is the default module
+  module = modules.get_current_module_name()
+  if module != 'default':
+    parts.append(module)
+
+  # If there is anything to prepend, add an extra blank entry for the trailing
+  # -dot-
+  if parts:
+    parts.append('')
+
+  return '-dot-'.join(parts)
+
+
+def get_app_hostname():
+  """Return hostname of a running Endpoints service.
+
+  Returns hostname of an running Endpoints API. It can be 1) "localhost:PORT"
+  if running on development server, or 2) "app_id.appspot.com" if running on
+  external app engine prod, or "app_id.googleplex.com" if running as Google
+  first-party Endpoints API, or 4) None if not running on App Engine
+  (e.g. Tornado Endpoints API).
+
+  Returns:
+    A string representing the hostname of the service.
+  """
+  if not is_running_on_app_engine() or is_running_on_localhost():
+    return None
+
+  app_id = app_identity.get_application_id()
+
+  prefix = get_hostname_prefix()
+  suffix = 'appspot.com'
+
+  if ':' in app_id:
+    tokens = app_id.split(':')
+    api_name = tokens[1]
+    if tokens[0] == 'google.com':
+      suffix = 'googleplex.com'
+  else:
+    api_name = app_id
+
+  return '{0}{1}.{2}'.format(prefix, api_name, suffix)
+
+
+def check_list_type(objects, allowed_type, name, allow_none=True):
+  """Verify that objects in list are of the allowed type or raise TypeError.
+
+  Args:
+    objects: The list of objects to check.
+    allowed_type: The allowed type of items in 'settings'.
+    name: Name of the list of objects, added to the exception.
+    allow_none: If set, None is also allowed.
+
+  Raises:
+    TypeError: if object is not of the allowed type.
+
+  Returns:
+    The list of objects, for convenient use in assignment.
+  """
+  if objects is None:
+    if not allow_none:
+      raise TypeError('%s is None, which is not allowed.' % name)
+    return objects
+  if not isinstance(objects, (tuple, list)):
+    raise TypeError('%s is not a list.' % name)
+  if not all(isinstance(i, allowed_type) for i in objects):
+    type_list = sorted(list(set(type(obj) for obj in objects)))
+    raise TypeError('%s contains types that don\'t match %s: %s' %
+                    (name, allowed_type.__name__, type_list))
+  return objects
+
+
+def snake_case_to_headless_camel_case(snake_string):
+  """Convert snake_case to headlessCamelCase.
+
+  Args:
+    snake_string: The string to be converted.
+  Returns:
+    The input string converted to headlessCamelCase.
+  """
+  return ''.join([snake_string.split('_')[0]] +
+                 list(sub_string.capitalize()
+                      for sub_string in snake_string.split('_')[1:]))