Merge branch 'main' into avm99963-monorail
Merged commit 34d8229ae2b51fb1a15bd208e6fe6185c94f6266
GitOrigin-RevId: 7ee0917f93a577e475f8e09526dd144d245593f4
diff --git a/third_party/protorpc/remote.py b/third_party/protorpc/remote.py
new file mode 100644
index 0000000..7983573
--- /dev/null
+++ b/third_party/protorpc/remote.py
@@ -0,0 +1,1248 @@
+#!/usr/bin/env python
+#
+# Copyright 2010 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.
+#
+
+"""Remote service library.
+
+This module contains classes that are useful for building remote services that
+conform to a standard request and response model. To conform to this model
+a service must be like the following class:
+
+ # Each service instance only handles a single request and is then discarded.
+ # Make these objects light weight.
+ class Service(object):
+
+ # It must be possible to construct service objects without any parameters.
+ # If your constructor needs extra information you should provide a
+ # no-argument factory function to create service instances.
+ def __init__(self):
+ ...
+
+ # Each remote method must use the 'method' decorator, passing the request
+ # and response message types. The remote method itself must take a single
+ # parameter which is an instance of RequestMessage and return an instance
+ # of ResponseMessage.
+ @method(RequestMessage, ResponseMessage)
+ def remote_method(self, request):
+ # Return an instance of ResponseMessage.
+
+ # A service object may optionally implement an 'initialize_request_state'
+ # method that takes as a parameter a single instance of a RequestState. If
+ # a service does not implement this method it will not receive the request
+ # state.
+ def initialize_request_state(self, state):
+ ...
+
+The 'Service' class is provided as a convenient base class that provides the
+above functionality. It implements all required and optional methods for a
+service. It also has convenience methods for creating factory functions that
+can pass persistent global state to a new service instance.
+
+The 'method' decorator is used to declare which methods of a class are
+meant to service RPCs. While this decorator is not responsible for handling
+actual remote method invocations, such as handling sockets, handling various
+RPC protocols and checking messages for correctness, it does attach information
+to methods that responsible classes can examine and ensure the correctness
+of the RPC.
+
+When the method decorator is used on a method, the wrapper method will have a
+'remote' property associated with it. The 'remote' property contains the
+request_type and response_type expected by the methods implementation.
+
+On its own, the method decorator does not provide any support for subclassing
+remote methods. In order to extend a service, one would need to redecorate
+the sub-classes methods. For example:
+
+ class MyService(Service):
+
+ @method(DoSomethingRequest, DoSomethingResponse)
+ def do_stuff(self, request):
+ ... implement do_stuff ...
+
+ class MyBetterService(MyService):
+
+ @method(DoSomethingRequest, DoSomethingResponse)
+ def do_stuff(self, request):
+ response = super(MyBetterService, self).do_stuff.remote.method(request)
+ ... do stuff with response ...
+ return response
+
+A Service subclass also has a Stub class that can be used with a transport for
+making RPCs. When a stub is created, it is capable of doing both synchronous
+and asynchronous RPCs if the underlying transport supports it. To make a stub
+using an HTTP transport do:
+
+ my_service = MyService.Stub(HttpTransport('<my service URL>'))
+
+For synchronous calls, just call the expected methods on the service stub:
+
+ request = DoSomethingRequest()
+ ...
+ response = my_service.do_something(request)
+
+Each stub instance has an async object that can be used for initiating
+asynchronous RPCs if the underlying protocol transport supports it. To
+make an asynchronous call, do:
+
+ rpc = my_service.async_.do_something(request)
+ response = rpc.get_response()
+"""
+
+from __future__ import with_statement
+import six
+
+__author__ = 'rafek@google.com (Rafe Kaplan)'
+
+import functools
+import logging
+import sys
+import threading
+from wsgiref import headers as wsgi_headers
+
+from . import message_types
+from . import messages
+from . import protobuf
+from . import protojson
+from . import util
+
+
+__all__ = [
+ 'ApplicationError',
+ 'MethodNotFoundError',
+ 'NetworkError',
+ 'RequestError',
+ 'RpcError',
+ 'ServerError',
+ 'ServiceConfigurationError',
+ 'ServiceDefinitionError',
+
+ 'HttpRequestState',
+ 'ProtocolConfig',
+ 'Protocols',
+ 'RequestState',
+ 'RpcState',
+ 'RpcStatus',
+ 'Service',
+ 'StubBase',
+ 'check_rpc_status',
+ 'get_remote_method_info',
+ 'is_error_status',
+ 'method',
+ 'remote',
+]
+
+
+class ServiceDefinitionError(messages.Error):
+ """Raised when a service is improperly defined."""
+
+
+class ServiceConfigurationError(messages.Error):
+ """Raised when a service is incorrectly configured."""
+
+
+# TODO: Use error_name to map to specific exception message types.
+class RpcStatus(messages.Message):
+ """Status of on-going or complete RPC.
+
+ Fields:
+ state: State of RPC.
+ error_name: Error name set by application. Only set when
+ status is APPLICATION_ERROR. For use by application to transmit
+ specific reason for error.
+ error_message: Error message associated with status.
+ """
+
+ class State(messages.Enum):
+ """Enumeration of possible RPC states.
+
+ Values:
+ OK: Completed successfully.
+ RUNNING: Still running, not complete.
+ REQUEST_ERROR: Request was malformed or incomplete.
+ SERVER_ERROR: Server experienced an unexpected error.
+ NETWORK_ERROR: An error occured on the network.
+ APPLICATION_ERROR: The application is indicating an error.
+ When in this state, RPC should also set application_error.
+ """
+ OK = 0
+ RUNNING = 1
+
+ REQUEST_ERROR = 2
+ SERVER_ERROR = 3
+ NETWORK_ERROR = 4
+ APPLICATION_ERROR = 5
+ METHOD_NOT_FOUND_ERROR = 6
+
+ state = messages.EnumField(State, 1, required=True)
+ error_message = messages.StringField(2)
+ error_name = messages.StringField(3)
+
+
+RpcState = RpcStatus.State
+
+
+class RpcError(messages.Error):
+ """Base class for RPC errors.
+
+ Each sub-class of RpcError is associated with an error value from RpcState
+ and has an attribute STATE that refers to that value.
+ """
+
+ def __init__(self, message, cause=None):
+ super(RpcError, self).__init__(message)
+ self.cause = cause
+
+ @classmethod
+ def from_state(cls, state):
+ """Get error class from RpcState.
+
+ Args:
+ state: RpcState value. Can be enum value itself, string or int.
+
+ Returns:
+ Exception class mapped to value if state is an error. Returns None
+ if state is OK or RUNNING.
+ """
+ return _RPC_STATE_TO_ERROR.get(RpcState(state))
+
+
+class RequestError(RpcError):
+ """Raised when wrong request objects received during method invocation."""
+
+ STATE = RpcState.REQUEST_ERROR
+
+
+class MethodNotFoundError(RequestError):
+ """Raised when unknown method requested by RPC."""
+
+ STATE = RpcState.METHOD_NOT_FOUND_ERROR
+
+
+class NetworkError(RpcError):
+ """Raised when network error occurs during RPC."""
+
+ STATE = RpcState.NETWORK_ERROR
+
+
+class ServerError(RpcError):
+ """Unexpected error occured on server."""
+
+ STATE = RpcState.SERVER_ERROR
+
+
+class ApplicationError(RpcError):
+ """Raised for application specific errors.
+
+ Attributes:
+ error_name: Application specific error name for exception.
+ """
+
+ STATE = RpcState.APPLICATION_ERROR
+
+ def __init__(self, message, error_name=None):
+ """Constructor.
+
+ Args:
+ message: Application specific error message.
+ error_name: Application specific error name. Must be None, string
+ or unicode string.
+ """
+ super(ApplicationError, self).__init__(message)
+ self.error_name = error_name
+
+ def __str__(self):
+ return self.args[0] or ''
+
+ def __repr__(self):
+ if self.error_name is None:
+ error_format = ''
+ else:
+ error_format = ', %r' % self.error_name
+ return '%s(%r%s)' % (type(self).__name__, self.args[0], error_format)
+
+
+_RPC_STATE_TO_ERROR = {
+ RpcState.REQUEST_ERROR: RequestError,
+ RpcState.NETWORK_ERROR: NetworkError,
+ RpcState.SERVER_ERROR: ServerError,
+ RpcState.APPLICATION_ERROR: ApplicationError,
+ RpcState.METHOD_NOT_FOUND_ERROR: MethodNotFoundError,
+}
+
+class _RemoteMethodInfo(object):
+ """Object for encapsulating remote method information.
+
+ An instance of this method is associated with the 'remote' attribute
+ of the methods 'invoke_remote_method' instance.
+
+ Instances of this class are created by the remote decorator and should not
+ be created directly.
+ """
+
+ def __init__(self,
+ method,
+ request_type,
+ response_type):
+ """Constructor.
+
+ Args:
+ method: The method which implements the remote method. This is a
+ function that will act as an instance method of a class definition
+ that is decorated by '@method'. It must always take 'self' as its
+ first parameter.
+ request_type: Expected request type for the remote method.
+ response_type: Expected response type for the remote method.
+ """
+ self.__method = method
+ self.__request_type = request_type
+ self.__response_type = response_type
+
+ @property
+ def method(self):
+ """Original undecorated method."""
+ return self.__method
+
+ @property
+ def request_type(self):
+ """Expected request type for remote method."""
+ if isinstance(self.__request_type, six.string_types):
+ self.__request_type = messages.find_definition(
+ self.__request_type,
+ relative_to=sys.modules[self.__method.__module__])
+ return self.__request_type
+
+ @property
+ def response_type(self):
+ """Expected response type for remote method."""
+ if isinstance(self.__response_type, six.string_types):
+ self.__response_type = messages.find_definition(
+ self.__response_type,
+ relative_to=sys.modules[self.__method.__module__])
+ return self.__response_type
+
+
+def method(request_type=message_types.VoidMessage,
+ response_type=message_types.VoidMessage):
+ """Method decorator for creating remote methods.
+
+ Args:
+ request_type: Message type of expected request.
+ response_type: Message type of expected response.
+
+ Returns:
+ 'remote_method_wrapper' function.
+
+ Raises:
+ TypeError: if the request_type or response_type parameters are not
+ proper subclasses of messages.Message.
+ """
+ if (not isinstance(request_type, six.string_types) and
+ (not isinstance(request_type, type) or
+ not issubclass(request_type, messages.Message) or
+ request_type is messages.Message)):
+ raise TypeError(
+ 'Must provide message class for request-type. Found %s',
+ request_type)
+
+ if (not isinstance(response_type, six.string_types) and
+ (not isinstance(response_type, type) or
+ not issubclass(response_type, messages.Message) or
+ response_type is messages.Message)):
+ raise TypeError(
+ 'Must provide message class for response-type. Found %s',
+ response_type)
+
+ def remote_method_wrapper(method):
+ """Decorator used to wrap method.
+
+ Args:
+ method: Original method being wrapped.
+
+ Returns:
+ 'invoke_remote_method' function responsible for actual invocation.
+ This invocation function instance is assigned an attribute 'remote'
+ which contains information about the remote method:
+ request_type: Expected request type for remote method.
+ response_type: Response type returned from remote method.
+
+ Raises:
+ TypeError: If request_type or response_type is not a subclass of Message
+ or is the Message class itself.
+ """
+
+ @functools.wraps(method)
+ def invoke_remote_method(service_instance, request):
+ """Function used to replace original method.
+
+ Invoke wrapped remote method. Checks to ensure that request and
+ response objects are the correct types.
+
+ Does not check whether messages are initialized.
+
+ Args:
+ service_instance: The service object whose method is being invoked.
+ This is passed to 'self' during the invocation of the original
+ method.
+ request: Request message.
+
+ Returns:
+ Results of calling wrapped remote method.
+
+ Raises:
+ RequestError: Request object is not of the correct type.
+ ServerError: Response object is not of the correct type.
+ """
+ if not isinstance(request, remote_method_info.request_type):
+ raise RequestError('Method %s.%s expected request type %s, '
+ 'received %s' %
+ (type(service_instance).__name__,
+ method.__name__,
+ remote_method_info.request_type,
+ type(request)))
+ response = method(service_instance, request)
+ if not isinstance(response, remote_method_info.response_type):
+ raise ServerError('Method %s.%s expected response type %s, '
+ 'sent %s' %
+ (type(service_instance).__name__,
+ method.__name__,
+ remote_method_info.response_type,
+ type(response)))
+ return response
+
+ remote_method_info = _RemoteMethodInfo(method,
+ request_type,
+ response_type)
+
+ invoke_remote_method.remote = remote_method_info
+ return invoke_remote_method
+
+ return remote_method_wrapper
+
+
+def remote(request_type, response_type):
+ """Temporary backward compatibility alias for method."""
+ logging.warning('The remote decorator has been renamed method. It will be '
+ 'removed in very soon from future versions of ProtoRPC.')
+ return method(request_type, response_type)
+
+
+def get_remote_method_info(method):
+ """Get remote method info object from remote method.
+
+ Returns:
+ Remote method info object if method is a remote method, else None.
+ """
+ if not callable(method):
+ return None
+
+ try:
+ method_info = method.remote
+ except AttributeError:
+ return None
+
+ if not isinstance(method_info, _RemoteMethodInfo):
+ return None
+
+ return method_info
+
+
+class StubBase(object):
+ """Base class for client side service stubs.
+
+ The remote method stubs are created by the _ServiceClass meta-class
+ when a Service class is first created. The resulting stub will
+ extend both this class and the service class it handles communications for.
+
+ Assume that there is a service:
+
+ class NewContactRequest(messages.Message):
+
+ name = messages.StringField(1, required=True)
+ phone = messages.StringField(2)
+ email = messages.StringField(3)
+
+ class NewContactResponse(message.Message):
+
+ contact_id = messages.StringField(1)
+
+ class AccountService(remote.Service):
+
+ @remote.method(NewContactRequest, NewContactResponse):
+ def new_contact(self, request):
+ ... implementation ...
+
+ A stub of this service can be called in two ways. The first is to pass in a
+ correctly initialized NewContactRequest message:
+
+ request = NewContactRequest()
+ request.name = 'Bob Somebody'
+ request.phone = '+1 415 555 1234'
+
+ response = account_service_stub.new_contact(request)
+
+ The second way is to pass in keyword parameters that correspond with the root
+ request message type:
+
+ account_service_stub.new_contact(name='Bob Somebody',
+ phone='+1 415 555 1234')
+
+ The second form will create a request message of the appropriate type.
+ """
+
+ def __init__(self, transport):
+ """Constructor.
+
+ Args:
+ transport: Underlying transport to communicate with remote service.
+ """
+ self.__transport = transport
+
+ @property
+ def transport(self):
+ """Transport used to communicate with remote service."""
+ return self.__transport
+
+
+class _ServiceClass(type):
+ """Meta-class for service class."""
+
+ def __new_async_method(cls, remote):
+ """Create asynchronous method for Async handler.
+
+ Args:
+ remote: RemoteInfo to create method for.
+ """
+ def async_method(self, *args, **kwargs):
+ """Asynchronous remote method.
+
+ Args:
+ self: Instance of StubBase.Async subclass.
+
+ Stub methods either take a single positional argument when a full
+ request message is passed in, or keyword arguments, but not both.
+
+ See docstring for StubBase for more information on how to use remote
+ stub methods.
+
+ Returns:
+ Rpc instance used to represent asynchronous RPC.
+ """
+ if args and kwargs:
+ raise TypeError('May not provide both args and kwargs')
+
+ if not args:
+ # Construct request object from arguments.
+ request = remote.request_type()
+ for name, value in six.iteritems(kwargs):
+ setattr(request, name, value)
+ else:
+ # First argument is request object.
+ request = args[0]
+
+ return self.transport.send_rpc(remote, request)
+
+ async_method.__name__ = remote.method.__name__
+ async_method = util.positional(2)(async_method)
+ async_method.remote = remote
+ return async_method
+
+ def __new_sync_method(cls, async_method):
+ """Create synchronous method for stub.
+
+ Args:
+ async_method: asynchronous method to delegate calls to.
+ """
+ def sync_method(self, *args, **kwargs):
+ """Synchronous remote method.
+
+ Args:
+ self: Instance of StubBase.Async subclass.
+ args: Tuple (request,):
+ request: Request object.
+ kwargs: Field values for request. Must be empty if request object
+ is provided.
+
+ Returns:
+ Response message from synchronized RPC.
+ """
+ return async_method(self.async_, *args, **kwargs).response
+ sync_method.__name__ = async_method.__name__
+ sync_method.remote = async_method.remote
+ return sync_method
+
+ def __create_async_methods(cls, remote_methods):
+ """Construct a dictionary of asynchronous methods based on remote methods.
+
+ Args:
+ remote_methods: Dictionary of methods with associated RemoteInfo objects.
+
+ Returns:
+ Dictionary of asynchronous methods with assocaited RemoteInfo objects.
+ Results added to AsyncStub subclass.
+ """
+ async_methods = {}
+ for method_name, method in remote_methods.items():
+ async_methods[method_name] = cls.__new_async_method(method.remote)
+ return async_methods
+
+ def __create_sync_methods(cls, async_methods):
+ """Construct a dictionary of synchronous methods based on remote methods.
+
+ Args:
+ async_methods: Dictionary of async methods to delegate calls to.
+
+ Returns:
+ Dictionary of synchronous methods with assocaited RemoteInfo objects.
+ Results added to Stub subclass.
+ """
+ sync_methods = {}
+ for method_name, async_method in async_methods.items():
+ sync_methods[method_name] = cls.__new_sync_method(async_method)
+ return sync_methods
+
+ def __new__(cls, name, bases, dct):
+ """Instantiate new service class instance."""
+ if StubBase not in bases:
+ # Collect existing remote methods.
+ base_methods = {}
+ for base in bases:
+ try:
+ remote_methods = base.__remote_methods
+ except AttributeError:
+ pass
+ else:
+ base_methods.update(remote_methods)
+
+ # Set this class private attribute so that base_methods do not have
+ # to be recacluated in __init__.
+ dct['_ServiceClass__base_methods'] = base_methods
+
+ for attribute, value in dct.items():
+ base_method = base_methods.get(attribute, None)
+ if base_method:
+ if not callable(value):
+ raise ServiceDefinitionError(
+ 'Must override %s in %s with a method.' % (
+ attribute, name))
+
+ if get_remote_method_info(value):
+ raise ServiceDefinitionError(
+ 'Do not use method decorator when overloading remote method %s '
+ 'on service %s.' %
+ (attribute, name))
+
+ base_remote_method_info = get_remote_method_info(base_method)
+ remote_decorator = method(
+ base_remote_method_info.request_type,
+ base_remote_method_info.response_type)
+ new_remote_method = remote_decorator(value)
+ dct[attribute] = new_remote_method
+
+ return type.__new__(cls, name, bases, dct)
+
+ def __init__(cls, name, bases, dct):
+ """Create uninitialized state on new class."""
+ type.__init__(cls, name, bases, dct)
+
+ # Only service implementation classes should have remote methods and stub
+ # sub classes created. Stub implementations have their own methods passed
+ # in to the type constructor.
+ if StubBase not in bases:
+ # Create list of remote methods.
+ cls.__remote_methods = dict(cls.__base_methods)
+
+ for attribute, value in dct.items():
+ value = getattr(cls, attribute)
+ remote_method_info = get_remote_method_info(value)
+ if remote_method_info:
+ cls.__remote_methods[attribute] = value
+
+ # Build asynchronous stub class.
+ stub_attributes = {'Service': cls}
+ async_methods = cls.__create_async_methods(cls.__remote_methods)
+ stub_attributes.update(async_methods)
+ async_class = type('AsyncStub', (StubBase, cls), stub_attributes)
+ cls.AsyncStub = async_class
+
+ # Constructor for synchronous stub class.
+ def __init__(self, transport):
+ """Constructor.
+
+ Args:
+ transport: Underlying transport to communicate with remote service.
+ """
+ super(cls.Stub, self).__init__(transport)
+ self.async_ = cls.AsyncStub(transport)
+
+ # Build synchronous stub class.
+ stub_attributes = {'Service': cls,
+ '__init__': __init__}
+ stub_attributes.update(cls.__create_sync_methods(async_methods))
+
+ cls.Stub = type('Stub', (StubBase, cls), stub_attributes)
+
+ @staticmethod
+ def all_remote_methods(cls):
+ """Get all remote methods of service.
+
+ Returns:
+ Dict from method name to unbound method.
+ """
+ return dict(cls.__remote_methods)
+
+
+class RequestState(object):
+ """Request state information.
+
+ Properties:
+ remote_host: Remote host name where request originated.
+ remote_address: IP address where request originated.
+ server_host: Host of server within which service resides.
+ server_port: Post which service has recevied request from.
+ """
+
+ @util.positional(1)
+ def __init__(self,
+ remote_host=None,
+ remote_address=None,
+ server_host=None,
+ server_port=None):
+ """Constructor.
+
+ Args:
+ remote_host: Assigned to property.
+ remote_address: Assigned to property.
+ server_host: Assigned to property.
+ server_port: Assigned to property.
+ """
+ self.__remote_host = remote_host
+ self.__remote_address = remote_address
+ self.__server_host = server_host
+ self.__server_port = server_port
+
+ @property
+ def remote_host(self):
+ return self.__remote_host
+
+ @property
+ def remote_address(self):
+ return self.__remote_address
+
+ @property
+ def server_host(self):
+ return self.__server_host
+
+ @property
+ def server_port(self):
+ return self.__server_port
+
+ def _repr_items(self):
+ for name in ['remote_host',
+ 'remote_address',
+ 'server_host',
+ 'server_port']:
+ yield name, getattr(self, name)
+
+ def __repr__(self):
+ """String representation of state."""
+ state = [self.__class__.__name__]
+ for name, value in self._repr_items():
+ if value:
+ state.append('%s=%r' % (name, value))
+
+ return '<%s>' % (' '.join(state),)
+
+
+class HttpRequestState(RequestState):
+ """HTTP request state information.
+
+ NOTE: Does not attempt to represent certain types of information from the
+ request such as the query string as query strings are not permitted in
+ ProtoRPC URLs unless required by the underlying message format.
+
+ Properties:
+ headers: wsgiref.headers.Headers instance of HTTP request headers.
+ http_method: HTTP method as a string.
+ service_path: Path on HTTP service where service is mounted. This path
+ will not include the remote method name.
+ """
+
+ @util.positional(1)
+ def __init__(self,
+ http_method=None,
+ service_path=None,
+ headers=None,
+ **kwargs):
+ """Constructor.
+
+ Args:
+ Same as RequestState, including:
+ http_method: Assigned to property.
+ service_path: Assigned to property.
+ headers: HTTP request headers. If instance of Headers, assigned to
+ property without copying. If dict, will convert to name value pairs
+ for use with Headers constructor. Otherwise, passed as parameters to
+ Headers constructor.
+ """
+ super(HttpRequestState, self).__init__(**kwargs)
+
+ self.__http_method = http_method
+ self.__service_path = service_path
+
+ # Initialize headers.
+ if isinstance(headers, dict):
+ header_list = []
+ for key, value in sorted(headers.items()):
+ if not isinstance(value, list):
+ value = [value]
+ for item in value:
+ header_list.append((key, item))
+ headers = header_list
+ self.__headers = wsgi_headers.Headers(headers or [])
+
+ @property
+ def http_method(self):
+ return self.__http_method
+
+ @property
+ def service_path(self):
+ return self.__service_path
+
+ @property
+ def headers(self):
+ return self.__headers
+
+ def _repr_items(self):
+ for item in super(HttpRequestState, self)._repr_items():
+ yield item
+
+ for name in ['http_method', 'service_path']:
+ yield name, getattr(self, name)
+
+ yield 'headers', list(self.headers.items())
+
+
+class Service(six.with_metaclass(_ServiceClass, object)):
+ """Service base class.
+
+ Base class used for defining remote services. Contains reflection functions,
+ useful helpers and built-in remote methods.
+
+ Services are expected to be constructed via either a constructor or factory
+ which takes no parameters. However, it might be required that some state or
+ configuration is passed in to a service across multiple requests.
+
+ To do this, define parameters to the constructor of the service and use
+ the 'new_factory' class method to build a constructor that will transmit
+ parameters to the constructor. For example:
+
+ class MyService(Service):
+
+ def __init__(self, configuration, state):
+ self.configuration = configuration
+ self.state = state
+
+ configuration = MyServiceConfiguration()
+ global_state = MyServiceState()
+
+ my_service_factory = MyService.new_factory(configuration,
+ state=global_state)
+
+ The contract with any service handler is that a new service object is created
+ to handle each user request, and that the construction does not take any
+ parameters. The factory satisfies this condition:
+
+ new_instance = my_service_factory()
+ assert new_instance.state is global_state
+
+ Attributes:
+ request_state: RequestState set via initialize_request_state.
+ """
+
+ __request_state = None
+
+ @classmethod
+ def all_remote_methods(cls):
+ """Get all remote methods for service class.
+
+ Built-in methods do not appear in the dictionary of remote methods.
+
+ Returns:
+ Dictionary mapping method name to remote method.
+ """
+ return _ServiceClass.all_remote_methods(cls)
+
+ @classmethod
+ def new_factory(cls, *args, **kwargs):
+ """Create factory for service.
+
+ Useful for passing configuration or state objects to the service. Accepts
+ arbitrary parameters and keywords, however, underlying service must accept
+ also accept not other parameters in its constructor.
+
+ Args:
+ args: Args to pass to service constructor.
+ kwargs: Keyword arguments to pass to service constructor.
+
+ Returns:
+ Factory function that will create a new instance and forward args and
+ keywords to the constructor.
+ """
+
+ def service_factory():
+ return cls(*args, **kwargs)
+
+ # Update docstring so that it is easier to debug.
+ full_class_name = '%s.%s' % (cls.__module__, cls.__name__)
+ service_factory.__doc__ = (
+ 'Creates new instances of service %s.\n\n'
+ 'Returns:\n'
+ ' New instance of %s.'
+ % (cls.__name__, full_class_name))
+
+ # Update name so that it is easier to debug the factory function.
+ service_factory.__name__ = '%s_service_factory' % cls.__name__
+
+ service_factory.service_class = cls
+
+ return service_factory
+
+ def initialize_request_state(self, request_state):
+ """Save request state for use in remote method.
+
+ Args:
+ request_state: RequestState instance.
+ """
+ self.__request_state = request_state
+
+ @classmethod
+ def definition_name(cls):
+ """Get definition name for Service class.
+
+ Package name is determined by the global 'package' attribute in the
+ module that contains the Service definition. If no 'package' attribute
+ is available, uses module name. If no module is found, just uses class
+ name as name.
+
+ Returns:
+ Fully qualified service name.
+ """
+ try:
+ return cls.__definition_name
+ except AttributeError:
+ outer_definition_name = cls.outer_definition_name()
+ if outer_definition_name is None:
+ cls.__definition_name = cls.__name__
+ else:
+ cls.__definition_name = '%s.%s' % (outer_definition_name, cls.__name__)
+
+ return cls.__definition_name
+
+ @classmethod
+ def outer_definition_name(cls):
+ """Get outer definition name.
+
+ Returns:
+ Package for service. Services are never nested inside other definitions.
+ """
+ return cls.definition_package()
+
+ @classmethod
+ def definition_package(cls):
+ """Get package for service.
+
+ Returns:
+ Package name for service.
+ """
+ try:
+ return cls.__definition_package
+ except AttributeError:
+ cls.__definition_package = util.get_package_for_module(cls.__module__)
+
+ return cls.__definition_package
+
+ @property
+ def request_state(self):
+ """Request state associated with this Service instance."""
+ return self.__request_state
+
+
+def is_error_status(status):
+ """Function that determines whether the RPC status is an error.
+
+ Args:
+ status: Initialized RpcStatus message to check for errors.
+ """
+ status.check_initialized()
+ return RpcError.from_state(status.state) is not None
+
+
+def check_rpc_status(status):
+ """Function converts an error status to a raised exception.
+
+ Args:
+ status: Initialized RpcStatus message to check for errors.
+
+ Raises:
+ RpcError according to state set on status, if it is an error state.
+ """
+ status.check_initialized()
+ error_class = RpcError.from_state(status.state)
+ if error_class is not None:
+ if error_class is ApplicationError:
+ raise error_class(status.error_message, status.error_name)
+ else:
+ raise error_class(status.error_message)
+
+
+class ProtocolConfig(object):
+ """Configuration for single protocol mapping.
+
+ A read-only protocol configuration provides a given protocol implementation
+ with a name and a set of content-types that it recognizes.
+
+ Properties:
+ protocol: The protocol implementation for configuration (usually a module,
+ for example, protojson, protobuf, etc.). This is an object that has the
+ following attributes:
+ CONTENT_TYPE: Used as the default content-type if default_content_type
+ is not set.
+ ALTERNATIVE_CONTENT_TYPES (optional): A list of alternative
+ content-types to the default that indicate the same protocol.
+ encode_message: Function that matches the signature of
+ ProtocolConfig.encode_message. Used for encoding a ProtoRPC message.
+ decode_message: Function that matches the signature of
+ ProtocolConfig.decode_message. Used for decoding a ProtoRPC message.
+ name: Name of protocol configuration.
+ default_content_type: The default content type for the protocol. Overrides
+ CONTENT_TYPE defined on protocol.
+ alternative_content_types: A list of alternative content-types supported
+ by the protocol. Must not contain the default content-type, nor
+ duplicates. Overrides ALTERNATIVE_CONTENT_TYPE defined on protocol.
+ content_types: A list of all content-types supported by configuration.
+ Combination of default content-type and alternatives.
+ """
+
+ def __init__(self,
+ protocol,
+ name,
+ default_content_type=None,
+ alternative_content_types=None):
+ """Constructor.
+
+ Args:
+ protocol: The protocol implementation for configuration.
+ name: The name of the protocol configuration.
+ default_content_type: The default content-type for protocol. If none
+ provided it will check protocol.CONTENT_TYPE.
+ alternative_content_types: A list of content-types. If none provided,
+ it will check protocol.ALTERNATIVE_CONTENT_TYPES. If that attribute
+ does not exist, will be an empty tuple.
+
+ Raises:
+ ServiceConfigurationError if there are any duplicate content-types.
+ """
+ self.__protocol = protocol
+ self.__name = name
+ self.__default_content_type = (default_content_type or
+ protocol.CONTENT_TYPE).lower()
+ if alternative_content_types is None:
+ alternative_content_types = getattr(protocol,
+ 'ALTERNATIVE_CONTENT_TYPES',
+ ())
+ self.__alternative_content_types = tuple(
+ content_type.lower() for content_type in alternative_content_types)
+ self.__content_types = (
+ (self.__default_content_type,) + self.__alternative_content_types)
+
+ # Detect duplicate content types in definition.
+ previous_type = None
+ for content_type in sorted(self.content_types):
+ if content_type == previous_type:
+ raise ServiceConfigurationError(
+ 'Duplicate content-type %s' % content_type)
+ previous_type = content_type
+
+ @property
+ def protocol(self):
+ return self.__protocol
+
+ @property
+ def name(self):
+ return self.__name
+
+ @property
+ def default_content_type(self):
+ return self.__default_content_type
+
+ @property
+ def alternate_content_types(self):
+ return self.__alternative_content_types
+
+ @property
+ def content_types(self):
+ return self.__content_types
+
+ def encode_message(self, message):
+ """Encode message.
+
+ Args:
+ message: Message instance to encode.
+
+ Returns:
+ String encoding of Message instance encoded in protocol's format.
+ """
+ return self.__protocol.encode_message(message)
+
+ def decode_message(self, message_type, encoded_message):
+ """Decode buffer to Message instance.
+
+ Args:
+ message_type: Message type to decode data to.
+ encoded_message: Encoded version of message as string.
+
+ Returns:
+ Decoded instance of message_type.
+ """
+ return self.__protocol.decode_message(message_type, encoded_message)
+
+
+class Protocols(object):
+ """Collection of protocol configurations.
+
+ Used to describe a complete set of content-type mappings for multiple
+ protocol configurations.
+
+ Properties:
+ names: Sorted list of the names of registered protocols.
+ content_types: Sorted list of supported content-types.
+ """
+
+ __default_protocols = None
+ __lock = threading.Lock()
+
+ def __init__(self):
+ """Constructor."""
+ self.__by_name = {}
+ self.__by_content_type = {}
+
+ def add_protocol_config(self, config):
+ """Add a protocol configuration to protocol mapping.
+
+ Args:
+ config: A ProtocolConfig.
+
+ Raises:
+ ServiceConfigurationError if protocol.name is already registered
+ or any of it's content-types are already registered.
+ """
+ if config.name in self.__by_name:
+ raise ServiceConfigurationError(
+ 'Protocol name %r is already in use' % config.name)
+ for content_type in config.content_types:
+ if content_type in self.__by_content_type:
+ raise ServiceConfigurationError(
+ 'Content type %r is already in use' % content_type)
+
+ self.__by_name[config.name] = config
+ self.__by_content_type.update((t, config) for t in config.content_types)
+
+ def add_protocol(self, *args, **kwargs):
+ """Add a protocol configuration from basic parameters.
+
+ Simple helper method that creates and registeres a ProtocolConfig instance.
+ """
+ self.add_protocol_config(ProtocolConfig(*args, **kwargs))
+
+ @property
+ def names(self):
+ return tuple(sorted(self.__by_name))
+
+ @property
+ def content_types(self):
+ return tuple(sorted(self.__by_content_type))
+
+ def lookup_by_name(self, name):
+ """Look up a ProtocolConfig by name.
+
+ Args:
+ name: Name of protocol to look for.
+
+ Returns:
+ ProtocolConfig associated with name.
+
+ Raises:
+ KeyError if there is no protocol for name.
+ """
+ return self.__by_name[name.lower()]
+
+ def lookup_by_content_type(self, content_type):
+ """Look up a ProtocolConfig by content-type.
+
+ Args:
+ content_type: Content-type to find protocol configuration for.
+
+ Returns:
+ ProtocolConfig associated with content-type.
+
+ Raises:
+ KeyError if there is no protocol for content-type.
+ """
+ return self.__by_content_type[content_type.lower()]
+
+ @classmethod
+ def new_default(cls):
+ """Create default protocols configuration.
+
+ Returns:
+ New Protocols instance configured for protobuf and protorpc.
+ """
+ protocols = cls()
+ protocols.add_protocol(protobuf, 'protobuf')
+ protocols.add_protocol(protojson.ProtoJson.get_default(), 'protojson')
+ return protocols
+
+ @classmethod
+ def get_default(cls):
+ """Get the global default Protocols instance.
+
+ Returns:
+ Current global default Protocols instance.
+ """
+ default_protocols = cls.__default_protocols
+ if default_protocols is None:
+ with cls.__lock:
+ default_protocols = cls.__default_protocols
+ if default_protocols is None:
+ default_protocols = cls.new_default()
+ cls.__default_protocols = default_protocols
+ return default_protocols
+
+ @classmethod
+ def set_default(cls, protocols):
+ """Set the global default Protocols instance.
+
+ Args:
+ protocols: A Protocols instance.
+
+ Raises:
+ TypeError: If protocols is not an instance of Protocols.
+ """
+ if not isinstance(protocols, Protocols):
+ raise TypeError(
+ 'Expected value of type "Protocols", found %r' % protocols)
+ with cls.__lock:
+ cls.__default_protocols = protocols