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