Index: tools/telemetry/third_party/gsutilz/third_party/protorpc/protorpc/remote.py |
diff --git a/tools/telemetry/third_party/gsutilz/third_party/protorpc/protorpc/remote.py b/tools/telemetry/third_party/gsutilz/third_party/protorpc/protorpc/remote.py |
new file mode 100755 |
index 0000000000000000000000000000000000000000..b6c97687196bb91cc7940eda42fab93769b75c10 |
--- /dev/null |
+++ b/tools/telemetry/third_party/gsutilz/third_party/protorpc/protorpc/remote.py |
@@ -0,0 +1,1247 @@ |
+#!/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 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] |
+ |
+ 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. |
+ """ |
+ |
+ 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 |
+ invoke_remote_method.__name__ = method.__name__ |
+ 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 |