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"""Handlers for remote services. 19 20This module contains classes that may be used to build a service 21on top of the App Engine Webapp framework. 22 23The services request handler can be configured to handle requests in a number 24of different request formats. All different request formats must have a way 25to map the request to the service handlers defined request message.Message 26class. The handler can also send a response in any format that can be mapped 27from the response message.Message class. 28 29Participants in an RPC: 30 31 There are four classes involved with the life cycle of an RPC. 32 33 Service factory: A user-defined service factory that is responsible for 34 instantiating an RPC service. The methods intended for use as RPC 35 methods must be decorated by the 'remote' decorator. 36 37 RPCMapper: Responsible for determining whether or not a specific request 38 matches a particular RPC format and translating between the actual 39 request/response and the underlying message types. A single instance of 40 an RPCMapper sub-class is required per service configuration. Each 41 mapper must be usable across multiple requests. 42 43 ServiceHandler: A webapp.RequestHandler sub-class that responds to the 44 webapp framework. It mediates between the RPCMapper and service 45 implementation class during a request. As determined by the Webapp 46 framework, a new ServiceHandler instance is created to handle each 47 user request. A handler is never used to handle more than one request. 48 49 ServiceHandlerFactory: A class that is responsible for creating new, 50 properly configured ServiceHandler instance for each request. The 51 factory is configured by providing it with a set of RPCMapper instances. 52 When the Webapp framework invokes the service handler, the handler 53 creates a new service class instance. The service class instance is 54 provided with a reference to the handler. A single instance of an 55 RPCMapper sub-class is required to configure each service. Each mapper 56 instance must be usable across multiple requests. 57 58RPC mappers: 59 60 RPC mappers translate between a single HTTP based RPC protocol and the 61 underlying service implementation. Each RPC mapper must configured 62 with the following information to determine if it is an appropriate 63 mapper for a given request: 64 65 http_methods: Set of HTTP methods supported by handler. 66 67 content_types: Set of supported content types. 68 69 default_content_type: Default content type for handler responses. 70 71 Built-in mapper implementations: 72 73 URLEncodedRPCMapper: Matches requests that are compatible with post 74 forms with the 'application/x-www-form-urlencoded' content-type 75 (this content type is the default if none is specified. It 76 translates post parameters into request parameters. 77 78 ProtobufRPCMapper: Matches requests that are compatible with post 79 forms with the 'application/x-google-protobuf' content-type. It 80 reads the contents of a binary post request. 81 82Public Exceptions: 83 Error: Base class for service handler errors. 84 ServiceConfigurationError: Raised when a service not correctly configured. 85 RequestError: Raised by RPC mappers when there is an error in its request 86 or request format. 87 ResponseError: Raised by RPC mappers when there is an error in its response. 88""" 89import six 90 91__author__ = 'rafek@google.com (Rafe Kaplan)' 92 93import six.moves.http_client 94import logging 95 96from .google_imports import webapp 97from .google_imports import webapp_util 98from .. import messages 99from .. import protobuf 100from .. import protojson 101from .. import protourlencode 102from .. import registry 103from .. import remote 104from .. import util 105from . import forms 106 107__all__ = [ 108 'Error', 109 'RequestError', 110 'ResponseError', 111 'ServiceConfigurationError', 112 113 'DEFAULT_REGISTRY_PATH', 114 115 'ProtobufRPCMapper', 116 'RPCMapper', 117 'ServiceHandler', 118 'ServiceHandlerFactory', 119 'URLEncodedRPCMapper', 120 'JSONRPCMapper', 121 'service_mapping', 122 'run_services', 123] 124 125 126class Error(Exception): 127 """Base class for all errors in service handlers module.""" 128 129 130class ServiceConfigurationError(Error): 131 """When service configuration is incorrect.""" 132 133 134class RequestError(Error): 135 """Error occurred when building request.""" 136 137 138class ResponseError(Error): 139 """Error occurred when building response.""" 140 141 142_URLENCODED_CONTENT_TYPE = protourlencode.CONTENT_TYPE 143_PROTOBUF_CONTENT_TYPE = protobuf.CONTENT_TYPE 144_JSON_CONTENT_TYPE = protojson.CONTENT_TYPE 145 146_EXTRA_JSON_CONTENT_TYPES = ['application/x-javascript', 147 'text/javascript', 148 'text/x-javascript', 149 'text/x-json', 150 'text/json', 151 ] 152 153# The whole method pattern is an optional regex. It contains a single 154# group used for mapping to the query parameter. This is passed to the 155# parameters of 'get' and 'post' on the ServiceHandler. 156_METHOD_PATTERN = r'(?:\.([^?]*))?' 157 158DEFAULT_REGISTRY_PATH = forms.DEFAULT_REGISTRY_PATH 159 160 161class RPCMapper(object): 162 """Interface to mediate between request and service object. 163 164 Request mappers are implemented to support various types of 165 RPC protocols. It is responsible for identifying whether a 166 given request matches a particular protocol, resolve the remote 167 method to invoke and mediate between the request and appropriate 168 protocol messages for the remote method. 169 """ 170 171 @util.positional(4) 172 def __init__(self, 173 http_methods, 174 default_content_type, 175 protocol, 176 content_types=None): 177 """Constructor. 178 179 Args: 180 http_methods: Set of HTTP methods supported by mapper. 181 default_content_type: Default content type supported by mapper. 182 protocol: The protocol implementation. Must implement encode_message and 183 decode_message. 184 content_types: Set of additionally supported content types. 185 """ 186 self.__http_methods = frozenset(http_methods) 187 self.__default_content_type = default_content_type 188 self.__protocol = protocol 189 190 if content_types is None: 191 content_types = [] 192 self.__content_types = frozenset([self.__default_content_type] + 193 content_types) 194 195 @property 196 def http_methods(self): 197 return self.__http_methods 198 199 @property 200 def default_content_type(self): 201 return self.__default_content_type 202 203 @property 204 def content_types(self): 205 return self.__content_types 206 207 def build_request(self, handler, request_type): 208 """Build request message based on request. 209 210 Each request mapper implementation is responsible for converting a 211 request to an appropriate message instance. 212 213 Args: 214 handler: RequestHandler instance that is servicing request. 215 Must be initialized with request object and been previously determined 216 to matching the protocol of the RPCMapper. 217 request_type: Message type to build. 218 219 Returns: 220 Instance of request_type populated by protocol buffer in request body. 221 222 Raises: 223 RequestError if the mapper implementation is not able to correctly 224 convert the request to the appropriate message. 225 """ 226 try: 227 return self.__protocol.decode_message(request_type, handler.request.body) 228 except (messages.ValidationError, messages.DecodeError) as err: 229 raise RequestError('Unable to parse request content: %s' % err) 230 231 def build_response(self, handler, response, pad_string=False): 232 """Build response based on service object response message. 233 234 Each request mapper implementation is responsible for converting a 235 response message to an appropriate handler response. 236 237 Args: 238 handler: RequestHandler instance that is servicing request. 239 Must be initialized with request object and been previously determined 240 to matching the protocol of the RPCMapper. 241 response: Response message as returned from the service object. 242 243 Raises: 244 ResponseError if the mapper implementation is not able to correctly 245 convert the message to an appropriate response. 246 """ 247 try: 248 encoded_message = self.__protocol.encode_message(response) 249 except messages.ValidationError as err: 250 raise ResponseError('Unable to encode message: %s' % err) 251 else: 252 handler.response.headers['Content-Type'] = self.default_content_type 253 handler.response.out.write(encoded_message) 254 255 256class ServiceHandlerFactory(object): 257 """Factory class used for instantiating new service handlers. 258 259 Normally a handler class is passed directly to the webapp framework 260 so that it can be simply instantiated to handle a single request. 261 The service handler, however, must be configured with additional 262 information so that it knows how to instantiate a service object. 263 This class acts the same as a normal RequestHandler class by 264 overriding the __call__ method to correctly configures a ServiceHandler 265 instance with a new service object. 266 267 The factory must also provide a set of RPCMapper instances which 268 examine a request to determine what protocol is being used and mediates 269 between the request and the service object. 270 271 The mapping of a service handler must have a single group indicating the 272 part of the URL path that maps to the request method. This group must 273 exist but can be optional for the request (the group may be followed by 274 '?' in the regular expression matching the request). 275 276 Usage: 277 278 stock_factory = ServiceHandlerFactory(StockService) 279 ... configure stock_factory by adding RPCMapper instances ... 280 281 application = webapp.WSGIApplication( 282 [stock_factory.mapping('/stocks')]) 283 284 Default usage: 285 286 application = webapp.WSGIApplication( 287 [ServiceHandlerFactory.default(StockService).mapping('/stocks')]) 288 """ 289 290 def __init__(self, service_factory): 291 """Constructor. 292 293 Args: 294 service_factory: Service factory to instantiate and provide to 295 service handler. 296 """ 297 self.__service_factory = service_factory 298 self.__request_mappers = [] 299 300 def all_request_mappers(self): 301 """Get all request mappers. 302 303 Returns: 304 Iterator of all request mappers used by this service factory. 305 """ 306 return iter(self.__request_mappers) 307 308 def add_request_mapper(self, mapper): 309 """Add request mapper to end of request mapper list.""" 310 self.__request_mappers.append(mapper) 311 312 def __call__(self): 313 """Construct a new service handler instance.""" 314 return ServiceHandler(self, self.__service_factory()) 315 316 @property 317 def service_factory(self): 318 """Service factory associated with this factory.""" 319 return self.__service_factory 320 321 @staticmethod 322 def __check_path(path): 323 """Check a path parameter. 324 325 Make sure a provided path parameter is compatible with the 326 webapp URL mapping. 327 328 Args: 329 path: Path to check. This is a plain path, not a regular expression. 330 331 Raises: 332 ValueError if path does not start with /, path ends with /. 333 """ 334 if path.endswith('/'): 335 raise ValueError('Path %s must not end with /.' % path) 336 337 def mapping(self, path): 338 """Convenience method to map service to application. 339 340 Args: 341 path: Path to map service to. It must be a simple path 342 with a leading / and no trailing /. 343 344 Returns: 345 Mapping from service URL to service handler factory. 346 """ 347 self.__check_path(path) 348 349 service_url_pattern = r'(%s)%s' % (path, _METHOD_PATTERN) 350 351 return service_url_pattern, self 352 353 @classmethod 354 def default(cls, service_factory, parameter_prefix=''): 355 """Convenience method to map default factory configuration to application. 356 357 Creates a standardized default service factory configuration that pre-maps 358 the URL encoded protocol handler to the factory. 359 360 Args: 361 service_factory: Service factory to instantiate and provide to 362 service handler. 363 method_parameter: The name of the form parameter used to determine the 364 method to invoke used by the URLEncodedRPCMapper. If None, no 365 parameter is used and the mapper will only match against the form 366 path-name. Defaults to 'method'. 367 parameter_prefix: If provided, all the parameters in the form are 368 expected to begin with that prefix by the URLEncodedRPCMapper. 369 370 Returns: 371 Mapping from service URL to service handler factory. 372 """ 373 factory = cls(service_factory) 374 375 factory.add_request_mapper(ProtobufRPCMapper()) 376 factory.add_request_mapper(JSONRPCMapper()) 377 378 return factory 379 380 381class ServiceHandler(webapp.RequestHandler): 382 """Web handler for RPC service. 383 384 Overridden methods: 385 get: All requests handled by 'handle' method. HTTP method stored in 386 attribute. Takes remote_method parameter as derived from the URL mapping. 387 post: All requests handled by 'handle' method. HTTP method stored in 388 attribute. Takes remote_method parameter as derived from the URL mapping. 389 redirect: Not implemented for this service handler. 390 391 New methods: 392 handle: Handle request for both GET and POST. 393 394 Attributes (in addition to attributes in RequestHandler): 395 service: Service instance associated with request being handled. 396 method: Method of request. Used by RPCMapper to determine match. 397 remote_method: Sub-path as provided to the 'get' and 'post' methods. 398 """ 399 400 def __init__(self, factory, service): 401 """Constructor. 402 403 Args: 404 factory: Instance of ServiceFactory used for constructing new service 405 instances used for handling requests. 406 service: Service instance used for handling RPC. 407 """ 408 self.__factory = factory 409 self.__service = service 410 411 @property 412 def service(self): 413 return self.__service 414 415 def __show_info(self, service_path, remote_method): 416 self.response.headers['content-type'] = 'text/plain; charset=utf-8' 417 response_message = [] 418 if remote_method: 419 response_message.append('%s.%s is a ProtoRPC method.\n\n' %( 420 service_path, remote_method)) 421 else: 422 response_message.append('%s is a ProtoRPC service.\n\n' % service_path) 423 definition_name_function = getattr(self.__service, 'definition_name', None) 424 if definition_name_function: 425 definition_name = definition_name_function() 426 else: 427 definition_name = '%s.%s' % (self.__service.__module__, 428 self.__service.__class__.__name__) 429 430 response_message.append('Service %s\n\n' % definition_name) 431 response_message.append('More about ProtoRPC: ') 432 433 response_message.append('http://code.google.com/p/google-protorpc\n') 434 self.response.out.write(util.pad_string(''.join(response_message))) 435 436 def get(self, service_path, remote_method): 437 """Handler method for GET requests. 438 439 Args: 440 service_path: Service path derived from request URL. 441 remote_method: Sub-path after service path has been matched. 442 """ 443 self.handle('GET', service_path, remote_method) 444 445 def post(self, service_path, remote_method): 446 """Handler method for POST requests. 447 448 Args: 449 service_path: Service path derived from request URL. 450 remote_method: Sub-path after service path has been matched. 451 """ 452 self.handle('POST', service_path, remote_method) 453 454 def redirect(self, uri, permanent=False): 455 """Not supported for services.""" 456 raise NotImplementedError('Services do not currently support redirection.') 457 458 def __send_error(self, 459 http_code, 460 status_state, 461 error_message, 462 mapper, 463 error_name=None): 464 status = remote.RpcStatus(state=status_state, 465 error_message=error_message, 466 error_name=error_name) 467 mapper.build_response(self, status) 468 self.response.headers['content-type'] = mapper.default_content_type 469 470 logging.error(error_message) 471 response_content = self.response.out.getvalue() 472 padding = ' ' * max(0, 512 - len(response_content)) 473 self.response.out.write(padding) 474 475 self.response.set_status(http_code, error_message) 476 477 def __send_simple_error(self, code, message, pad=True): 478 """Send error to caller without embedded message.""" 479 self.response.headers['content-type'] = 'text/plain; charset=utf-8' 480 logging.error(message) 481 self.response.set_status(code, message) 482 483 response_message = six.moves.http_client.responses.get(code, 'Unknown Error') 484 if pad: 485 response_message = util.pad_string(response_message) 486 self.response.out.write(response_message) 487 488 def __get_content_type(self): 489 content_type = self.request.headers.get('content-type', None) 490 if not content_type: 491 content_type = self.request.environ.get('HTTP_CONTENT_TYPE', None) 492 if not content_type: 493 return None 494 495 # Lop off parameters from the end (for example content-encoding) 496 return content_type.split(';', 1)[0].lower() 497 498 def __headers(self, content_type): 499 for name in self.request.headers: 500 name = name.lower() 501 if name == 'content-type': 502 value = content_type 503 elif name == 'content-length': 504 value = str(len(self.request.body)) 505 else: 506 value = self.request.headers.get(name, '') 507 yield name, value 508 509 def handle(self, http_method, service_path, remote_method): 510 """Handle a service request. 511 512 The handle method will handle either a GET or POST response. 513 It is up to the individual mappers from the handler factory to determine 514 which request methods they can service. 515 516 If the protocol is not recognized, the request does not provide a correct 517 request for that protocol or the service object does not support the 518 requested RPC method, will return error code 400 in the response. 519 520 Args: 521 http_method: HTTP method of request. 522 service_path: Service path derived from request URL. 523 remote_method: Sub-path after service path has been matched. 524 """ 525 self.response.headers['x-content-type-options'] = 'nosniff' 526 if not remote_method and http_method == 'GET': 527 # Special case a normal get request, presumably via a browser. 528 self.error(405) 529 self.__show_info(service_path, remote_method) 530 return 531 532 content_type = self.__get_content_type() 533 534 # Provide server state to the service. If the service object does not have 535 # an "initialize_request_state" method, will not attempt to assign state. 536 try: 537 state_initializer = self.service.initialize_request_state 538 except AttributeError: 539 pass 540 else: 541 server_port = self.request.environ.get('SERVER_PORT', None) 542 if server_port: 543 server_port = int(server_port) 544 545 request_state = remote.HttpRequestState( 546 remote_host=self.request.environ.get('REMOTE_HOST', None), 547 remote_address=self.request.environ.get('REMOTE_ADDR', None), 548 server_host=self.request.environ.get('SERVER_HOST', None), 549 server_port=server_port, 550 http_method=http_method, 551 service_path=service_path, 552 headers=list(self.__headers(content_type))) 553 state_initializer(request_state) 554 555 if not content_type: 556 self.__send_simple_error(400, 'Invalid RPC request: missing content-type') 557 return 558 559 # Search for mapper to mediate request. 560 for mapper in self.__factory.all_request_mappers(): 561 if content_type in mapper.content_types: 562 break 563 else: 564 if http_method == 'GET': 565 self.error(six.moves.http_client.UNSUPPORTED_MEDIA_TYPE) 566 self.__show_info(service_path, remote_method) 567 else: 568 self.__send_simple_error(six.moves.http_client.UNSUPPORTED_MEDIA_TYPE, 569 'Unsupported content-type: %s' % content_type) 570 return 571 572 try: 573 if http_method not in mapper.http_methods: 574 if http_method == 'GET': 575 self.error(six.moves.http_client.METHOD_NOT_ALLOWED) 576 self.__show_info(service_path, remote_method) 577 else: 578 self.__send_simple_error(six.moves.http_client.METHOD_NOT_ALLOWED, 579 'Unsupported HTTP method: %s' % http_method) 580 return 581 582 try: 583 try: 584 method = getattr(self.service, remote_method) 585 method_info = method.remote 586 except AttributeError as err: 587 self.__send_error( 588 400, remote.RpcState.METHOD_NOT_FOUND_ERROR, 589 'Unrecognized RPC method: %s' % remote_method, 590 mapper) 591 return 592 593 request = mapper.build_request(self, method_info.request_type) 594 except (RequestError, messages.DecodeError) as err: 595 self.__send_error(400, 596 remote.RpcState.REQUEST_ERROR, 597 'Error parsing ProtoRPC request (%s)' % err, 598 mapper) 599 return 600 601 try: 602 response = method(request) 603 except remote.ApplicationError as err: 604 self.__send_error(400, 605 remote.RpcState.APPLICATION_ERROR, 606 err.message, 607 mapper, 608 err.error_name) 609 return 610 611 mapper.build_response(self, response) 612 except Exception as err: 613 logging.error('An unexpected error occured when handling RPC: %s', 614 err, exc_info=1) 615 616 self.__send_error(500, 617 remote.RpcState.SERVER_ERROR, 618 'Internal Server Error', 619 mapper) 620 return 621 622 623# TODO(rafek): Support tag-id only forms. 624class URLEncodedRPCMapper(RPCMapper): 625 """Request mapper for application/x-www-form-urlencoded forms. 626 627 This mapper is useful for building forms that can invoke RPC. Many services 628 are also configured to work using URL encoded request information because 629 of its perceived ease of programming and debugging. 630 631 The mapper must be provided with at least method_parameter or 632 remote_method_pattern so that it is possible to determine how to determine the 633 requests RPC method. If both are provided, the service will respond to both 634 method request types, however, only one may be present in a given request. 635 If both types are detected, the request will not match. 636 """ 637 638 def __init__(self, parameter_prefix=''): 639 """Constructor. 640 641 Args: 642 parameter_prefix: If provided, all the parameters in the form are 643 expected to begin with that prefix. 644 """ 645 # Private attributes: 646 # __parameter_prefix: parameter prefix as provided by constructor 647 # parameter. 648 super(URLEncodedRPCMapper, self).__init__(['POST'], 649 _URLENCODED_CONTENT_TYPE, 650 self) 651 self.__parameter_prefix = parameter_prefix 652 653 def encode_message(self, message): 654 """Encode a message using parameter prefix. 655 656 Args: 657 message: Message to URL Encode. 658 659 Returns: 660 URL encoded message. 661 """ 662 return protourlencode.encode_message(message, 663 prefix=self.__parameter_prefix) 664 665 @property 666 def parameter_prefix(self): 667 """Prefix all form parameters are expected to begin with.""" 668 return self.__parameter_prefix 669 670 def build_request(self, handler, request_type): 671 """Build request from URL encoded HTTP request. 672 673 Constructs message from names of URL encoded parameters. If this service 674 handler has a parameter prefix, parameters must begin with it or are 675 ignored. 676 677 Args: 678 handler: RequestHandler instance that is servicing request. 679 request_type: Message type to build. 680 681 Returns: 682 Instance of request_type populated by protocol buffer in request 683 parameters. 684 685 Raises: 686 RequestError if message type contains nested message field or repeated 687 message field. Will raise RequestError if there are any repeated 688 parameters. 689 """ 690 request = request_type() 691 builder = protourlencode.URLEncodedRequestBuilder( 692 request, prefix=self.__parameter_prefix) 693 for argument in sorted(handler.request.arguments()): 694 values = handler.request.get_all(argument) 695 try: 696 builder.add_parameter(argument, values) 697 except messages.DecodeError as err: 698 raise RequestError(str(err)) 699 return request 700 701 702class ProtobufRPCMapper(RPCMapper): 703 """Request mapper for application/x-protobuf service requests. 704 705 This mapper will parse protocol buffer from a POST body and return the request 706 as a protocol buffer. 707 """ 708 709 def __init__(self): 710 super(ProtobufRPCMapper, self).__init__(['POST'], 711 _PROTOBUF_CONTENT_TYPE, 712 protobuf) 713 714 715class JSONRPCMapper(RPCMapper): 716 """Request mapper for application/x-protobuf service requests. 717 718 This mapper will parse protocol buffer from a POST body and return the request 719 as a protocol buffer. 720 """ 721 722 def __init__(self): 723 super(JSONRPCMapper, self).__init__( 724 ['POST'], 725 _JSON_CONTENT_TYPE, 726 protojson, 727 content_types=_EXTRA_JSON_CONTENT_TYPES) 728 729 730def service_mapping(services, 731 registry_path=DEFAULT_REGISTRY_PATH): 732 """Create a services mapping for use with webapp. 733 734 Creates basic default configuration and registration for ProtoRPC services. 735 Each service listed in the service mapping has a standard service handler 736 factory created for it. 737 738 The list of mappings can either be an explicit path to service mapping or 739 just services. If mappings are just services, they will automatically 740 be mapped to their default name. For exampel: 741 742 package = 'my_package' 743 744 class MyService(remote.Service): 745 ... 746 747 server_mapping([('/my_path', MyService), # Maps to /my_path 748 MyService, # Maps to /my_package/MyService 749 ]) 750 751 Specifying a service mapping: 752 753 Normally services are mapped to URL paths by specifying a tuple 754 (path, service): 755 path: The path the service resides on. 756 service: The service class or service factory for creating new instances 757 of the service. For more information about service factories, please 758 see remote.Service.new_factory. 759 760 If no tuple is provided, and therefore no path specified, a default path 761 is calculated by using the fully qualified service name using a URL path 762 separator for each of its components instead of a '.'. 763 764 Args: 765 services: Can be service type, service factory or string definition name of 766 service being mapped or list of tuples (path, service): 767 path: Path on server to map service to. 768 service: Service type, service factory or string definition name of 769 service being mapped. 770 Can also be a dict. If so, the keys are treated as the path and values as 771 the service. 772 registry_path: Path to give to registry service. Use None to disable 773 registry service. 774 775 Returns: 776 List of tuples defining a mapping of request handlers compatible with a 777 webapp application. 778 779 Raises: 780 ServiceConfigurationError when duplicate paths are provided. 781 """ 782 if isinstance(services, dict): 783 services = six.iteritems(services) 784 mapping = [] 785 registry_map = {} 786 787 if registry_path is not None: 788 registry_service = registry.RegistryService.new_factory(registry_map) 789 services = list(services) + [(registry_path, registry_service)] 790 mapping.append((registry_path + r'/form(?:/)?', 791 forms.FormsHandler.new_factory(registry_path))) 792 mapping.append((registry_path + r'/form/(.+)', forms.ResourceHandler)) 793 794 paths = set() 795 for service_item in services: 796 infer_path = not isinstance(service_item, (list, tuple)) 797 if infer_path: 798 service = service_item 799 else: 800 service = service_item[1] 801 802 service_class = getattr(service, 'service_class', service) 803 804 if infer_path: 805 path = '/' + service_class.definition_name().replace('.', '/') 806 else: 807 path = service_item[0] 808 809 if path in paths: 810 raise ServiceConfigurationError( 811 'Path %r is already defined in service mapping' % path.encode('utf-8')) 812 else: 813 paths.add(path) 814 815 # Create service mapping for webapp. 816 new_mapping = ServiceHandlerFactory.default(service).mapping(path) 817 mapping.append(new_mapping) 818 819 # Update registry with service class. 820 registry_map[path] = service_class 821 822 return mapping 823 824 825def run_services(services, 826 registry_path=DEFAULT_REGISTRY_PATH): 827 """Handle CGI request using service mapping. 828 829 Args: 830 Same as service_mapping. 831 """ 832 mappings = service_mapping(services, registry_path=registry_path) 833 application = webapp.WSGIApplication(mappings) 834 webapp_util.run_wsgi_app(application) 835