blob: 798357323990e477e72c03c6862be5b8f467e390 [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001#!/usr/bin/env python
2#
3# Copyright 2010 Google Inc.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17
18"""Remote service library.
19
20This module contains classes that are useful for building remote services that
21conform to a standard request and response model. To conform to this model
22a service must be like the following class:
23
24 # Each service instance only handles a single request and is then discarded.
25 # Make these objects light weight.
26 class Service(object):
27
28 # It must be possible to construct service objects without any parameters.
29 # If your constructor needs extra information you should provide a
30 # no-argument factory function to create service instances.
31 def __init__(self):
32 ...
33
34 # Each remote method must use the 'method' decorator, passing the request
35 # and response message types. The remote method itself must take a single
36 # parameter which is an instance of RequestMessage and return an instance
37 # of ResponseMessage.
38 @method(RequestMessage, ResponseMessage)
39 def remote_method(self, request):
40 # Return an instance of ResponseMessage.
41
42 # A service object may optionally implement an 'initialize_request_state'
43 # method that takes as a parameter a single instance of a RequestState. If
44 # a service does not implement this method it will not receive the request
45 # state.
46 def initialize_request_state(self, state):
47 ...
48
49The 'Service' class is provided as a convenient base class that provides the
50above functionality. It implements all required and optional methods for a
51service. It also has convenience methods for creating factory functions that
52can pass persistent global state to a new service instance.
53
54The 'method' decorator is used to declare which methods of a class are
55meant to service RPCs. While this decorator is not responsible for handling
56actual remote method invocations, such as handling sockets, handling various
57RPC protocols and checking messages for correctness, it does attach information
58to methods that responsible classes can examine and ensure the correctness
59of the RPC.
60
61When the method decorator is used on a method, the wrapper method will have a
62'remote' property associated with it. The 'remote' property contains the
63request_type and response_type expected by the methods implementation.
64
65On its own, the method decorator does not provide any support for subclassing
66remote methods. In order to extend a service, one would need to redecorate
67the sub-classes methods. For example:
68
69 class MyService(Service):
70
71 @method(DoSomethingRequest, DoSomethingResponse)
72 def do_stuff(self, request):
73 ... implement do_stuff ...
74
75 class MyBetterService(MyService):
76
77 @method(DoSomethingRequest, DoSomethingResponse)
78 def do_stuff(self, request):
79 response = super(MyBetterService, self).do_stuff.remote.method(request)
80 ... do stuff with response ...
81 return response
82
83A Service subclass also has a Stub class that can be used with a transport for
84making RPCs. When a stub is created, it is capable of doing both synchronous
85and asynchronous RPCs if the underlying transport supports it. To make a stub
86using an HTTP transport do:
87
88 my_service = MyService.Stub(HttpTransport('<my service URL>'))
89
90For synchronous calls, just call the expected methods on the service stub:
91
92 request = DoSomethingRequest()
93 ...
94 response = my_service.do_something(request)
95
96Each stub instance has an async object that can be used for initiating
97asynchronous RPCs if the underlying protocol transport supports it. To
98make an asynchronous call, do:
99
100 rpc = my_service.async_.do_something(request)
101 response = rpc.get_response()
102"""
103
104from __future__ import with_statement
105import six
106
107__author__ = 'rafek@google.com (Rafe Kaplan)'
108
109import functools
110import logging
111import sys
112import threading
113from wsgiref import headers as wsgi_headers
114
115from . import message_types
116from . import messages
117from . import protobuf
118from . import protojson
119from . import util
120
121
122__all__ = [
123 'ApplicationError',
124 'MethodNotFoundError',
125 'NetworkError',
126 'RequestError',
127 'RpcError',
128 'ServerError',
129 'ServiceConfigurationError',
130 'ServiceDefinitionError',
131
132 'HttpRequestState',
133 'ProtocolConfig',
134 'Protocols',
135 'RequestState',
136 'RpcState',
137 'RpcStatus',
138 'Service',
139 'StubBase',
140 'check_rpc_status',
141 'get_remote_method_info',
142 'is_error_status',
143 'method',
144 'remote',
145]
146
147
148class ServiceDefinitionError(messages.Error):
149 """Raised when a service is improperly defined."""
150
151
152class ServiceConfigurationError(messages.Error):
153 """Raised when a service is incorrectly configured."""
154
155
156# TODO: Use error_name to map to specific exception message types.
157class RpcStatus(messages.Message):
158 """Status of on-going or complete RPC.
159
160 Fields:
161 state: State of RPC.
162 error_name: Error name set by application. Only set when
163 status is APPLICATION_ERROR. For use by application to transmit
164 specific reason for error.
165 error_message: Error message associated with status.
166 """
167
168 class State(messages.Enum):
169 """Enumeration of possible RPC states.
170
171 Values:
172 OK: Completed successfully.
173 RUNNING: Still running, not complete.
174 REQUEST_ERROR: Request was malformed or incomplete.
175 SERVER_ERROR: Server experienced an unexpected error.
176 NETWORK_ERROR: An error occured on the network.
177 APPLICATION_ERROR: The application is indicating an error.
178 When in this state, RPC should also set application_error.
179 """
180 OK = 0
181 RUNNING = 1
182
183 REQUEST_ERROR = 2
184 SERVER_ERROR = 3
185 NETWORK_ERROR = 4
186 APPLICATION_ERROR = 5
187 METHOD_NOT_FOUND_ERROR = 6
188
189 state = messages.EnumField(State, 1, required=True)
190 error_message = messages.StringField(2)
191 error_name = messages.StringField(3)
192
193
194RpcState = RpcStatus.State
195
196
197class RpcError(messages.Error):
198 """Base class for RPC errors.
199
200 Each sub-class of RpcError is associated with an error value from RpcState
201 and has an attribute STATE that refers to that value.
202 """
203
204 def __init__(self, message, cause=None):
205 super(RpcError, self).__init__(message)
206 self.cause = cause
207
208 @classmethod
209 def from_state(cls, state):
210 """Get error class from RpcState.
211
212 Args:
213 state: RpcState value. Can be enum value itself, string or int.
214
215 Returns:
216 Exception class mapped to value if state is an error. Returns None
217 if state is OK or RUNNING.
218 """
219 return _RPC_STATE_TO_ERROR.get(RpcState(state))
220
221
222class RequestError(RpcError):
223 """Raised when wrong request objects received during method invocation."""
224
225 STATE = RpcState.REQUEST_ERROR
226
227
228class MethodNotFoundError(RequestError):
229 """Raised when unknown method requested by RPC."""
230
231 STATE = RpcState.METHOD_NOT_FOUND_ERROR
232
233
234class NetworkError(RpcError):
235 """Raised when network error occurs during RPC."""
236
237 STATE = RpcState.NETWORK_ERROR
238
239
240class ServerError(RpcError):
241 """Unexpected error occured on server."""
242
243 STATE = RpcState.SERVER_ERROR
244
245
246class ApplicationError(RpcError):
247 """Raised for application specific errors.
248
249 Attributes:
250 error_name: Application specific error name for exception.
251 """
252
253 STATE = RpcState.APPLICATION_ERROR
254
255 def __init__(self, message, error_name=None):
256 """Constructor.
257
258 Args:
259 message: Application specific error message.
260 error_name: Application specific error name. Must be None, string
261 or unicode string.
262 """
263 super(ApplicationError, self).__init__(message)
264 self.error_name = error_name
265
266 def __str__(self):
267 return self.args[0] or ''
268
269 def __repr__(self):
270 if self.error_name is None:
271 error_format = ''
272 else:
273 error_format = ', %r' % self.error_name
274 return '%s(%r%s)' % (type(self).__name__, self.args[0], error_format)
275
276
277_RPC_STATE_TO_ERROR = {
278 RpcState.REQUEST_ERROR: RequestError,
279 RpcState.NETWORK_ERROR: NetworkError,
280 RpcState.SERVER_ERROR: ServerError,
281 RpcState.APPLICATION_ERROR: ApplicationError,
282 RpcState.METHOD_NOT_FOUND_ERROR: MethodNotFoundError,
283}
284
285class _RemoteMethodInfo(object):
286 """Object for encapsulating remote method information.
287
288 An instance of this method is associated with the 'remote' attribute
289 of the methods 'invoke_remote_method' instance.
290
291 Instances of this class are created by the remote decorator and should not
292 be created directly.
293 """
294
295 def __init__(self,
296 method,
297 request_type,
298 response_type):
299 """Constructor.
300
301 Args:
302 method: The method which implements the remote method. This is a
303 function that will act as an instance method of a class definition
304 that is decorated by '@method'. It must always take 'self' as its
305 first parameter.
306 request_type: Expected request type for the remote method.
307 response_type: Expected response type for the remote method.
308 """
309 self.__method = method
310 self.__request_type = request_type
311 self.__response_type = response_type
312
313 @property
314 def method(self):
315 """Original undecorated method."""
316 return self.__method
317
318 @property
319 def request_type(self):
320 """Expected request type for remote method."""
321 if isinstance(self.__request_type, six.string_types):
322 self.__request_type = messages.find_definition(
323 self.__request_type,
324 relative_to=sys.modules[self.__method.__module__])
325 return self.__request_type
326
327 @property
328 def response_type(self):
329 """Expected response type for remote method."""
330 if isinstance(self.__response_type, six.string_types):
331 self.__response_type = messages.find_definition(
332 self.__response_type,
333 relative_to=sys.modules[self.__method.__module__])
334 return self.__response_type
335
336
337def method(request_type=message_types.VoidMessage,
338 response_type=message_types.VoidMessage):
339 """Method decorator for creating remote methods.
340
341 Args:
342 request_type: Message type of expected request.
343 response_type: Message type of expected response.
344
345 Returns:
346 'remote_method_wrapper' function.
347
348 Raises:
349 TypeError: if the request_type or response_type parameters are not
350 proper subclasses of messages.Message.
351 """
352 if (not isinstance(request_type, six.string_types) and
353 (not isinstance(request_type, type) or
354 not issubclass(request_type, messages.Message) or
355 request_type is messages.Message)):
356 raise TypeError(
357 'Must provide message class for request-type. Found %s',
358 request_type)
359
360 if (not isinstance(response_type, six.string_types) and
361 (not isinstance(response_type, type) or
362 not issubclass(response_type, messages.Message) or
363 response_type is messages.Message)):
364 raise TypeError(
365 'Must provide message class for response-type. Found %s',
366 response_type)
367
368 def remote_method_wrapper(method):
369 """Decorator used to wrap method.
370
371 Args:
372 method: Original method being wrapped.
373
374 Returns:
375 'invoke_remote_method' function responsible for actual invocation.
376 This invocation function instance is assigned an attribute 'remote'
377 which contains information about the remote method:
378 request_type: Expected request type for remote method.
379 response_type: Response type returned from remote method.
380
381 Raises:
382 TypeError: If request_type or response_type is not a subclass of Message
383 or is the Message class itself.
384 """
385
386 @functools.wraps(method)
387 def invoke_remote_method(service_instance, request):
388 """Function used to replace original method.
389
390 Invoke wrapped remote method. Checks to ensure that request and
391 response objects are the correct types.
392
393 Does not check whether messages are initialized.
394
395 Args:
396 service_instance: The service object whose method is being invoked.
397 This is passed to 'self' during the invocation of the original
398 method.
399 request: Request message.
400
401 Returns:
402 Results of calling wrapped remote method.
403
404 Raises:
405 RequestError: Request object is not of the correct type.
406 ServerError: Response object is not of the correct type.
407 """
408 if not isinstance(request, remote_method_info.request_type):
409 raise RequestError('Method %s.%s expected request type %s, '
410 'received %s' %
411 (type(service_instance).__name__,
412 method.__name__,
413 remote_method_info.request_type,
414 type(request)))
415 response = method(service_instance, request)
416 if not isinstance(response, remote_method_info.response_type):
417 raise ServerError('Method %s.%s expected response type %s, '
418 'sent %s' %
419 (type(service_instance).__name__,
420 method.__name__,
421 remote_method_info.response_type,
422 type(response)))
423 return response
424
425 remote_method_info = _RemoteMethodInfo(method,
426 request_type,
427 response_type)
428
429 invoke_remote_method.remote = remote_method_info
430 return invoke_remote_method
431
432 return remote_method_wrapper
433
434
435def remote(request_type, response_type):
436 """Temporary backward compatibility alias for method."""
437 logging.warning('The remote decorator has been renamed method. It will be '
438 'removed in very soon from future versions of ProtoRPC.')
439 return method(request_type, response_type)
440
441
442def get_remote_method_info(method):
443 """Get remote method info object from remote method.
444
445 Returns:
446 Remote method info object if method is a remote method, else None.
447 """
448 if not callable(method):
449 return None
450
451 try:
452 method_info = method.remote
453 except AttributeError:
454 return None
455
456 if not isinstance(method_info, _RemoteMethodInfo):
457 return None
458
459 return method_info
460
461
462class StubBase(object):
463 """Base class for client side service stubs.
464
465 The remote method stubs are created by the _ServiceClass meta-class
466 when a Service class is first created. The resulting stub will
467 extend both this class and the service class it handles communications for.
468
469 Assume that there is a service:
470
471 class NewContactRequest(messages.Message):
472
473 name = messages.StringField(1, required=True)
474 phone = messages.StringField(2)
475 email = messages.StringField(3)
476
477 class NewContactResponse(message.Message):
478
479 contact_id = messages.StringField(1)
480
481 class AccountService(remote.Service):
482
483 @remote.method(NewContactRequest, NewContactResponse):
484 def new_contact(self, request):
485 ... implementation ...
486
487 A stub of this service can be called in two ways. The first is to pass in a
488 correctly initialized NewContactRequest message:
489
490 request = NewContactRequest()
491 request.name = 'Bob Somebody'
492 request.phone = '+1 415 555 1234'
493
494 response = account_service_stub.new_contact(request)
495
496 The second way is to pass in keyword parameters that correspond with the root
497 request message type:
498
499 account_service_stub.new_contact(name='Bob Somebody',
500 phone='+1 415 555 1234')
501
502 The second form will create a request message of the appropriate type.
503 """
504
505 def __init__(self, transport):
506 """Constructor.
507
508 Args:
509 transport: Underlying transport to communicate with remote service.
510 """
511 self.__transport = transport
512
513 @property
514 def transport(self):
515 """Transport used to communicate with remote service."""
516 return self.__transport
517
518
519class _ServiceClass(type):
520 """Meta-class for service class."""
521
522 def __new_async_method(cls, remote):
523 """Create asynchronous method for Async handler.
524
525 Args:
526 remote: RemoteInfo to create method for.
527 """
528 def async_method(self, *args, **kwargs):
529 """Asynchronous remote method.
530
531 Args:
532 self: Instance of StubBase.Async subclass.
533
534 Stub methods either take a single positional argument when a full
535 request message is passed in, or keyword arguments, but not both.
536
537 See docstring for StubBase for more information on how to use remote
538 stub methods.
539
540 Returns:
541 Rpc instance used to represent asynchronous RPC.
542 """
543 if args and kwargs:
544 raise TypeError('May not provide both args and kwargs')
545
546 if not args:
547 # Construct request object from arguments.
548 request = remote.request_type()
549 for name, value in six.iteritems(kwargs):
550 setattr(request, name, value)
551 else:
552 # First argument is request object.
553 request = args[0]
554
555 return self.transport.send_rpc(remote, request)
556
557 async_method.__name__ = remote.method.__name__
558 async_method = util.positional(2)(async_method)
559 async_method.remote = remote
560 return async_method
561
562 def __new_sync_method(cls, async_method):
563 """Create synchronous method for stub.
564
565 Args:
566 async_method: asynchronous method to delegate calls to.
567 """
568 def sync_method(self, *args, **kwargs):
569 """Synchronous remote method.
570
571 Args:
572 self: Instance of StubBase.Async subclass.
573 args: Tuple (request,):
574 request: Request object.
575 kwargs: Field values for request. Must be empty if request object
576 is provided.
577
578 Returns:
579 Response message from synchronized RPC.
580 """
581 return async_method(self.async_, *args, **kwargs).response
582 sync_method.__name__ = async_method.__name__
583 sync_method.remote = async_method.remote
584 return sync_method
585
586 def __create_async_methods(cls, remote_methods):
587 """Construct a dictionary of asynchronous methods based on remote methods.
588
589 Args:
590 remote_methods: Dictionary of methods with associated RemoteInfo objects.
591
592 Returns:
593 Dictionary of asynchronous methods with assocaited RemoteInfo objects.
594 Results added to AsyncStub subclass.
595 """
596 async_methods = {}
597 for method_name, method in remote_methods.items():
598 async_methods[method_name] = cls.__new_async_method(method.remote)
599 return async_methods
600
601 def __create_sync_methods(cls, async_methods):
602 """Construct a dictionary of synchronous methods based on remote methods.
603
604 Args:
605 async_methods: Dictionary of async methods to delegate calls to.
606
607 Returns:
608 Dictionary of synchronous methods with assocaited RemoteInfo objects.
609 Results added to Stub subclass.
610 """
611 sync_methods = {}
612 for method_name, async_method in async_methods.items():
613 sync_methods[method_name] = cls.__new_sync_method(async_method)
614 return sync_methods
615
616 def __new__(cls, name, bases, dct):
617 """Instantiate new service class instance."""
618 if StubBase not in bases:
619 # Collect existing remote methods.
620 base_methods = {}
621 for base in bases:
622 try:
623 remote_methods = base.__remote_methods
624 except AttributeError:
625 pass
626 else:
627 base_methods.update(remote_methods)
628
629 # Set this class private attribute so that base_methods do not have
630 # to be recacluated in __init__.
631 dct['_ServiceClass__base_methods'] = base_methods
632
633 for attribute, value in dct.items():
634 base_method = base_methods.get(attribute, None)
635 if base_method:
636 if not callable(value):
637 raise ServiceDefinitionError(
638 'Must override %s in %s with a method.' % (
639 attribute, name))
640
641 if get_remote_method_info(value):
642 raise ServiceDefinitionError(
643 'Do not use method decorator when overloading remote method %s '
644 'on service %s.' %
645 (attribute, name))
646
647 base_remote_method_info = get_remote_method_info(base_method)
648 remote_decorator = method(
649 base_remote_method_info.request_type,
650 base_remote_method_info.response_type)
651 new_remote_method = remote_decorator(value)
652 dct[attribute] = new_remote_method
653
654 return type.__new__(cls, name, bases, dct)
655
656 def __init__(cls, name, bases, dct):
657 """Create uninitialized state on new class."""
658 type.__init__(cls, name, bases, dct)
659
660 # Only service implementation classes should have remote methods and stub
661 # sub classes created. Stub implementations have their own methods passed
662 # in to the type constructor.
663 if StubBase not in bases:
664 # Create list of remote methods.
665 cls.__remote_methods = dict(cls.__base_methods)
666
667 for attribute, value in dct.items():
668 value = getattr(cls, attribute)
669 remote_method_info = get_remote_method_info(value)
670 if remote_method_info:
671 cls.__remote_methods[attribute] = value
672
673 # Build asynchronous stub class.
674 stub_attributes = {'Service': cls}
675 async_methods = cls.__create_async_methods(cls.__remote_methods)
676 stub_attributes.update(async_methods)
677 async_class = type('AsyncStub', (StubBase, cls), stub_attributes)
678 cls.AsyncStub = async_class
679
680 # Constructor for synchronous stub class.
681 def __init__(self, transport):
682 """Constructor.
683
684 Args:
685 transport: Underlying transport to communicate with remote service.
686 """
687 super(cls.Stub, self).__init__(transport)
688 self.async_ = cls.AsyncStub(transport)
689
690 # Build synchronous stub class.
691 stub_attributes = {'Service': cls,
692 '__init__': __init__}
693 stub_attributes.update(cls.__create_sync_methods(async_methods))
694
695 cls.Stub = type('Stub', (StubBase, cls), stub_attributes)
696
697 @staticmethod
698 def all_remote_methods(cls):
699 """Get all remote methods of service.
700
701 Returns:
702 Dict from method name to unbound method.
703 """
704 return dict(cls.__remote_methods)
705
706
707class RequestState(object):
708 """Request state information.
709
710 Properties:
711 remote_host: Remote host name where request originated.
712 remote_address: IP address where request originated.
713 server_host: Host of server within which service resides.
714 server_port: Post which service has recevied request from.
715 """
716
717 @util.positional(1)
718 def __init__(self,
719 remote_host=None,
720 remote_address=None,
721 server_host=None,
722 server_port=None):
723 """Constructor.
724
725 Args:
726 remote_host: Assigned to property.
727 remote_address: Assigned to property.
728 server_host: Assigned to property.
729 server_port: Assigned to property.
730 """
731 self.__remote_host = remote_host
732 self.__remote_address = remote_address
733 self.__server_host = server_host
734 self.__server_port = server_port
735
736 @property
737 def remote_host(self):
738 return self.__remote_host
739
740 @property
741 def remote_address(self):
742 return self.__remote_address
743
744 @property
745 def server_host(self):
746 return self.__server_host
747
748 @property
749 def server_port(self):
750 return self.__server_port
751
752 def _repr_items(self):
753 for name in ['remote_host',
754 'remote_address',
755 'server_host',
756 'server_port']:
757 yield name, getattr(self, name)
758
759 def __repr__(self):
760 """String representation of state."""
761 state = [self.__class__.__name__]
762 for name, value in self._repr_items():
763 if value:
764 state.append('%s=%r' % (name, value))
765
766 return '<%s>' % (' '.join(state),)
767
768
769class HttpRequestState(RequestState):
770 """HTTP request state information.
771
772 NOTE: Does not attempt to represent certain types of information from the
773 request such as the query string as query strings are not permitted in
774 ProtoRPC URLs unless required by the underlying message format.
775
776 Properties:
777 headers: wsgiref.headers.Headers instance of HTTP request headers.
778 http_method: HTTP method as a string.
779 service_path: Path on HTTP service where service is mounted. This path
780 will not include the remote method name.
781 """
782
783 @util.positional(1)
784 def __init__(self,
785 http_method=None,
786 service_path=None,
787 headers=None,
788 **kwargs):
789 """Constructor.
790
791 Args:
792 Same as RequestState, including:
793 http_method: Assigned to property.
794 service_path: Assigned to property.
795 headers: HTTP request headers. If instance of Headers, assigned to
796 property without copying. If dict, will convert to name value pairs
797 for use with Headers constructor. Otherwise, passed as parameters to
798 Headers constructor.
799 """
800 super(HttpRequestState, self).__init__(**kwargs)
801
802 self.__http_method = http_method
803 self.__service_path = service_path
804
805 # Initialize headers.
806 if isinstance(headers, dict):
807 header_list = []
808 for key, value in sorted(headers.items()):
809 if not isinstance(value, list):
810 value = [value]
811 for item in value:
812 header_list.append((key, item))
813 headers = header_list
814 self.__headers = wsgi_headers.Headers(headers or [])
815
816 @property
817 def http_method(self):
818 return self.__http_method
819
820 @property
821 def service_path(self):
822 return self.__service_path
823
824 @property
825 def headers(self):
826 return self.__headers
827
828 def _repr_items(self):
829 for item in super(HttpRequestState, self)._repr_items():
830 yield item
831
832 for name in ['http_method', 'service_path']:
833 yield name, getattr(self, name)
834
835 yield 'headers', list(self.headers.items())
836
837
838class Service(six.with_metaclass(_ServiceClass, object)):
839 """Service base class.
840
841 Base class used for defining remote services. Contains reflection functions,
842 useful helpers and built-in remote methods.
843
844 Services are expected to be constructed via either a constructor or factory
845 which takes no parameters. However, it might be required that some state or
846 configuration is passed in to a service across multiple requests.
847
848 To do this, define parameters to the constructor of the service and use
849 the 'new_factory' class method to build a constructor that will transmit
850 parameters to the constructor. For example:
851
852 class MyService(Service):
853
854 def __init__(self, configuration, state):
855 self.configuration = configuration
856 self.state = state
857
858 configuration = MyServiceConfiguration()
859 global_state = MyServiceState()
860
861 my_service_factory = MyService.new_factory(configuration,
862 state=global_state)
863
864 The contract with any service handler is that a new service object is created
865 to handle each user request, and that the construction does not take any
866 parameters. The factory satisfies this condition:
867
868 new_instance = my_service_factory()
869 assert new_instance.state is global_state
870
871 Attributes:
872 request_state: RequestState set via initialize_request_state.
873 """
874
875 __request_state = None
876
877 @classmethod
878 def all_remote_methods(cls):
879 """Get all remote methods for service class.
880
881 Built-in methods do not appear in the dictionary of remote methods.
882
883 Returns:
884 Dictionary mapping method name to remote method.
885 """
886 return _ServiceClass.all_remote_methods(cls)
887
888 @classmethod
889 def new_factory(cls, *args, **kwargs):
890 """Create factory for service.
891
892 Useful for passing configuration or state objects to the service. Accepts
893 arbitrary parameters and keywords, however, underlying service must accept
894 also accept not other parameters in its constructor.
895
896 Args:
897 args: Args to pass to service constructor.
898 kwargs: Keyword arguments to pass to service constructor.
899
900 Returns:
901 Factory function that will create a new instance and forward args and
902 keywords to the constructor.
903 """
904
905 def service_factory():
906 return cls(*args, **kwargs)
907
908 # Update docstring so that it is easier to debug.
909 full_class_name = '%s.%s' % (cls.__module__, cls.__name__)
910 service_factory.__doc__ = (
911 'Creates new instances of service %s.\n\n'
912 'Returns:\n'
913 ' New instance of %s.'
914 % (cls.__name__, full_class_name))
915
916 # Update name so that it is easier to debug the factory function.
917 service_factory.__name__ = '%s_service_factory' % cls.__name__
918
919 service_factory.service_class = cls
920
921 return service_factory
922
923 def initialize_request_state(self, request_state):
924 """Save request state for use in remote method.
925
926 Args:
927 request_state: RequestState instance.
928 """
929 self.__request_state = request_state
930
931 @classmethod
932 def definition_name(cls):
933 """Get definition name for Service class.
934
935 Package name is determined by the global 'package' attribute in the
936 module that contains the Service definition. If no 'package' attribute
937 is available, uses module name. If no module is found, just uses class
938 name as name.
939
940 Returns:
941 Fully qualified service name.
942 """
943 try:
944 return cls.__definition_name
945 except AttributeError:
946 outer_definition_name = cls.outer_definition_name()
947 if outer_definition_name is None:
948 cls.__definition_name = cls.__name__
949 else:
950 cls.__definition_name = '%s.%s' % (outer_definition_name, cls.__name__)
951
952 return cls.__definition_name
953
954 @classmethod
955 def outer_definition_name(cls):
956 """Get outer definition name.
957
958 Returns:
959 Package for service. Services are never nested inside other definitions.
960 """
961 return cls.definition_package()
962
963 @classmethod
964 def definition_package(cls):
965 """Get package for service.
966
967 Returns:
968 Package name for service.
969 """
970 try:
971 return cls.__definition_package
972 except AttributeError:
973 cls.__definition_package = util.get_package_for_module(cls.__module__)
974
975 return cls.__definition_package
976
977 @property
978 def request_state(self):
979 """Request state associated with this Service instance."""
980 return self.__request_state
981
982
983def is_error_status(status):
984 """Function that determines whether the RPC status is an error.
985
986 Args:
987 status: Initialized RpcStatus message to check for errors.
988 """
989 status.check_initialized()
990 return RpcError.from_state(status.state) is not None
991
992
993def check_rpc_status(status):
994 """Function converts an error status to a raised exception.
995
996 Args:
997 status: Initialized RpcStatus message to check for errors.
998
999 Raises:
1000 RpcError according to state set on status, if it is an error state.
1001 """
1002 status.check_initialized()
1003 error_class = RpcError.from_state(status.state)
1004 if error_class is not None:
1005 if error_class is ApplicationError:
1006 raise error_class(status.error_message, status.error_name)
1007 else:
1008 raise error_class(status.error_message)
1009
1010
1011class ProtocolConfig(object):
1012 """Configuration for single protocol mapping.
1013
1014 A read-only protocol configuration provides a given protocol implementation
1015 with a name and a set of content-types that it recognizes.
1016
1017 Properties:
1018 protocol: The protocol implementation for configuration (usually a module,
1019 for example, protojson, protobuf, etc.). This is an object that has the
1020 following attributes:
1021 CONTENT_TYPE: Used as the default content-type if default_content_type
1022 is not set.
1023 ALTERNATIVE_CONTENT_TYPES (optional): A list of alternative
1024 content-types to the default that indicate the same protocol.
1025 encode_message: Function that matches the signature of
1026 ProtocolConfig.encode_message. Used for encoding a ProtoRPC message.
1027 decode_message: Function that matches the signature of
1028 ProtocolConfig.decode_message. Used for decoding a ProtoRPC message.
1029 name: Name of protocol configuration.
1030 default_content_type: The default content type for the protocol. Overrides
1031 CONTENT_TYPE defined on protocol.
1032 alternative_content_types: A list of alternative content-types supported
1033 by the protocol. Must not contain the default content-type, nor
1034 duplicates. Overrides ALTERNATIVE_CONTENT_TYPE defined on protocol.
1035 content_types: A list of all content-types supported by configuration.
1036 Combination of default content-type and alternatives.
1037 """
1038
1039 def __init__(self,
1040 protocol,
1041 name,
1042 default_content_type=None,
1043 alternative_content_types=None):
1044 """Constructor.
1045
1046 Args:
1047 protocol: The protocol implementation for configuration.
1048 name: The name of the protocol configuration.
1049 default_content_type: The default content-type for protocol. If none
1050 provided it will check protocol.CONTENT_TYPE.
1051 alternative_content_types: A list of content-types. If none provided,
1052 it will check protocol.ALTERNATIVE_CONTENT_TYPES. If that attribute
1053 does not exist, will be an empty tuple.
1054
1055 Raises:
1056 ServiceConfigurationError if there are any duplicate content-types.
1057 """
1058 self.__protocol = protocol
1059 self.__name = name
1060 self.__default_content_type = (default_content_type or
1061 protocol.CONTENT_TYPE).lower()
1062 if alternative_content_types is None:
1063 alternative_content_types = getattr(protocol,
1064 'ALTERNATIVE_CONTENT_TYPES',
1065 ())
1066 self.__alternative_content_types = tuple(
1067 content_type.lower() for content_type in alternative_content_types)
1068 self.__content_types = (
1069 (self.__default_content_type,) + self.__alternative_content_types)
1070
1071 # Detect duplicate content types in definition.
1072 previous_type = None
1073 for content_type in sorted(self.content_types):
1074 if content_type == previous_type:
1075 raise ServiceConfigurationError(
1076 'Duplicate content-type %s' % content_type)
1077 previous_type = content_type
1078
1079 @property
1080 def protocol(self):
1081 return self.__protocol
1082
1083 @property
1084 def name(self):
1085 return self.__name
1086
1087 @property
1088 def default_content_type(self):
1089 return self.__default_content_type
1090
1091 @property
1092 def alternate_content_types(self):
1093 return self.__alternative_content_types
1094
1095 @property
1096 def content_types(self):
1097 return self.__content_types
1098
1099 def encode_message(self, message):
1100 """Encode message.
1101
1102 Args:
1103 message: Message instance to encode.
1104
1105 Returns:
1106 String encoding of Message instance encoded in protocol's format.
1107 """
1108 return self.__protocol.encode_message(message)
1109
1110 def decode_message(self, message_type, encoded_message):
1111 """Decode buffer to Message instance.
1112
1113 Args:
1114 message_type: Message type to decode data to.
1115 encoded_message: Encoded version of message as string.
1116
1117 Returns:
1118 Decoded instance of message_type.
1119 """
1120 return self.__protocol.decode_message(message_type, encoded_message)
1121
1122
1123class Protocols(object):
1124 """Collection of protocol configurations.
1125
1126 Used to describe a complete set of content-type mappings for multiple
1127 protocol configurations.
1128
1129 Properties:
1130 names: Sorted list of the names of registered protocols.
1131 content_types: Sorted list of supported content-types.
1132 """
1133
1134 __default_protocols = None
1135 __lock = threading.Lock()
1136
1137 def __init__(self):
1138 """Constructor."""
1139 self.__by_name = {}
1140 self.__by_content_type = {}
1141
1142 def add_protocol_config(self, config):
1143 """Add a protocol configuration to protocol mapping.
1144
1145 Args:
1146 config: A ProtocolConfig.
1147
1148 Raises:
1149 ServiceConfigurationError if protocol.name is already registered
1150 or any of it's content-types are already registered.
1151 """
1152 if config.name in self.__by_name:
1153 raise ServiceConfigurationError(
1154 'Protocol name %r is already in use' % config.name)
1155 for content_type in config.content_types:
1156 if content_type in self.__by_content_type:
1157 raise ServiceConfigurationError(
1158 'Content type %r is already in use' % content_type)
1159
1160 self.__by_name[config.name] = config
1161 self.__by_content_type.update((t, config) for t in config.content_types)
1162
1163 def add_protocol(self, *args, **kwargs):
1164 """Add a protocol configuration from basic parameters.
1165
1166 Simple helper method that creates and registeres a ProtocolConfig instance.
1167 """
1168 self.add_protocol_config(ProtocolConfig(*args, **kwargs))
1169
1170 @property
1171 def names(self):
1172 return tuple(sorted(self.__by_name))
1173
1174 @property
1175 def content_types(self):
1176 return tuple(sorted(self.__by_content_type))
1177
1178 def lookup_by_name(self, name):
1179 """Look up a ProtocolConfig by name.
1180
1181 Args:
1182 name: Name of protocol to look for.
1183
1184 Returns:
1185 ProtocolConfig associated with name.
1186
1187 Raises:
1188 KeyError if there is no protocol for name.
1189 """
1190 return self.__by_name[name.lower()]
1191
1192 def lookup_by_content_type(self, content_type):
1193 """Look up a ProtocolConfig by content-type.
1194
1195 Args:
1196 content_type: Content-type to find protocol configuration for.
1197
1198 Returns:
1199 ProtocolConfig associated with content-type.
1200
1201 Raises:
1202 KeyError if there is no protocol for content-type.
1203 """
1204 return self.__by_content_type[content_type.lower()]
1205
1206 @classmethod
1207 def new_default(cls):
1208 """Create default protocols configuration.
1209
1210 Returns:
1211 New Protocols instance configured for protobuf and protorpc.
1212 """
1213 protocols = cls()
1214 protocols.add_protocol(protobuf, 'protobuf')
1215 protocols.add_protocol(protojson.ProtoJson.get_default(), 'protojson')
1216 return protocols
1217
1218 @classmethod
1219 def get_default(cls):
1220 """Get the global default Protocols instance.
1221
1222 Returns:
1223 Current global default Protocols instance.
1224 """
1225 default_protocols = cls.__default_protocols
1226 if default_protocols is None:
1227 with cls.__lock:
1228 default_protocols = cls.__default_protocols
1229 if default_protocols is None:
1230 default_protocols = cls.new_default()
1231 cls.__default_protocols = default_protocols
1232 return default_protocols
1233
1234 @classmethod
1235 def set_default(cls, protocols):
1236 """Set the global default Protocols instance.
1237
1238 Args:
1239 protocols: A Protocols instance.
1240
1241 Raises:
1242 TypeError: If protocols is not an instance of Protocols.
1243 """
1244 if not isinstance(protocols, Protocols):
1245 raise TypeError(
1246 'Expected value of type "Protocols", found %r' % protocols)
1247 with cls.__lock:
1248 cls.__default_protocols = protocols