App Engine Python SDK version 1.9.12
[gae.git] / python / lib / endpoints-1.0 / endpoints / api_config.py
blob103a075d4b61d03f8e250d3a0ec1711f71dd946e
1 #!/usr/bin/env python
3 # Copyright 2007 Google Inc.
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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.
19 """Library for generating an API configuration document for a ProtoRPC backend.
21 The protorpc.remote.Service is inspected and a JSON document describing
22 the API is returned.
24 class MyResponse(messages.Message):
25 bool_value = messages.BooleanField(1)
26 int32_value = messages.IntegerField(2)
28 class MyService(remote.Service):
30 @remote.method(message_types.VoidMessage, MyResponse)
31 def entries_get(self, request):
32 pass
34 api = ApiConfigGenerator().pretty_print_config_to_json(MyService)
35 """
47 try:
48 import json
49 except ImportError:
50 import simplejson as json
51 import logging
52 import re
54 from endpoints import message_parser
55 from endpoints import users_id_token
56 from protorpc import message_types
57 from protorpc import messages
58 from protorpc import remote
59 from protorpc import util
61 try:
63 from google.appengine.api import app_identity
64 except ImportError:
66 from google.appengine.api import app_identity
69 __all__ = [
70 'API_EXPLORER_CLIENT_ID',
71 'ApiAuth',
72 'ApiConfigGenerator',
73 'ApiConfigurationError',
74 'ApiFrontEndLimitRule',
75 'ApiFrontEndLimits',
76 'CacheControl',
77 'ResourceContainer',
78 'EMAIL_SCOPE',
79 'api',
80 'method',
81 'AUTH_LEVEL'
85 API_EXPLORER_CLIENT_ID = '292824132082.apps.googleusercontent.com'
86 EMAIL_SCOPE = 'https://www.googleapis.com/auth/userinfo.email'
87 _PATH_VARIABLE_PATTERN = r'{([a-zA-Z_][a-zA-Z_.\d]*)}'
89 _MULTICLASS_MISMATCH_ERROR_TEMPLATE = (
90 'Attempting to implement service %s, version %s, with multiple '
91 'classes that aren\'t compatible. See docstring for api() for '
92 'examples how to implement a multi-class API.')
95 def _Enum(docstring, *names):
96 """Utility to generate enum classes used by annotations.
98 Args:
99 docstring: Docstring for the generated enum class.
100 *names: Enum names.
102 Returns:
103 A class that contains enum names as attributes.
105 enums = dict(zip(names, range(len(names))))
106 reverse = dict((value, key) for key, value in enums.iteritems())
107 enums['reverse_mapping'] = reverse
108 enums['__doc__'] = docstring
109 return type('Enum', (object,), enums)
111 _AUTH_LEVEL_DOCSTRING = """
112 Define the enums used by the auth_level annotation to specify frontend
113 authentication requirement.
115 Frontend authentication is handled by a Google API server prior to the
116 request reaching backends. An early return before hitting the backend can
117 happen if the request does not fulfil the requirement specified by the
118 auth_level.
120 Valid values of auth_level and their meanings are:
122 AUTH_LEVEL.REQUIRED: Valid authentication credentials are required. Backend
123 will be called only if authentication credentials are present and valid.
125 AUTH_LEVEL.OPTIONAL: Authentication is optional. If authentication credentials
126 are supplied they must be valid. Backend will be called if the request
127 contains valid authentication credentials or no authentication credentials.
129 AUTH_LEVEL.OPTIONAL_CONTINUE: Authentication is optional and will be attempted
130 if authentication credentials are supplied. Invalid authentication
131 credentials will be removed but the request can always reach backend.
133 AUTH_LEVEL.NONE: Frontend authentication will be skipped. If authentication is
134 desired, it will need to be performed by the backend.
137 AUTH_LEVEL = _Enum(_AUTH_LEVEL_DOCSTRING, 'REQUIRED', 'OPTIONAL',
138 'OPTIONAL_CONTINUE', 'NONE')
141 class ApiConfigurationError(Exception):
142 """Exception thrown if there's an error in the configuration/annotations."""
145 def _GetFieldAttributes(field):
146 """Decomposes field into the needed arguments to pass to the constructor.
148 This can be used to create copies of the field or to compare if two fields
149 are "equal" (since __eq__ is not implemented on messages.Field).
151 Args:
152 field: A ProtoRPC message field (potentially to be copied).
154 Raises:
155 TypeError: If the field is not an instance of messages.Field.
157 Returns:
158 A pair of relevant arguments to be passed to the constructor for the field
159 type. The first element is a list of positional arguments for the
160 constructor and the second is a dictionary of keyword arguments.
162 if not isinstance(field, messages.Field):
163 raise TypeError('Field %r to be copied not a ProtoRPC field.' % (field,))
165 positional_args = []
166 kwargs = {
167 'required': field.required,
168 'repeated': field.repeated,
169 'variant': field.variant,
170 'default': field._Field__default,
173 if isinstance(field, messages.MessageField):
175 kwargs.pop('default')
176 if not isinstance(field, message_types.DateTimeField):
177 positional_args.insert(0, field.message_type)
178 elif isinstance(field, messages.EnumField):
179 positional_args.insert(0, field.type)
181 return positional_args, kwargs
184 def _CopyField(field, number=None):
185 """Copies a (potentially) owned ProtoRPC field instance into a new copy.
187 Args:
188 field: A ProtoRPC message field to be copied.
189 number: An integer for the field to override the number of the field.
190 Defaults to None.
192 Raises:
193 TypeError: If the field is not an instance of messages.Field.
195 Returns:
196 A copy of the ProtoRPC message field.
198 positional_args, kwargs = _GetFieldAttributes(field)
199 number = number or field.number
200 positional_args.append(number)
201 return field.__class__(*positional_args, **kwargs)
204 def _CompareFields(field, other_field):
205 """Checks if two ProtoRPC fields are "equal".
207 Compares the arguments, rather than the id of the elements (which is
208 the default __eq__ behavior) as well as the class of the fields.
210 Args:
211 field: A ProtoRPC message field to be compared.
212 other_field: A ProtoRPC message field to be compared.
214 Returns:
215 Boolean indicating whether the fields are equal.
217 field_attrs = _GetFieldAttributes(field)
218 other_field_attrs = _GetFieldAttributes(other_field)
219 if field_attrs != other_field_attrs:
220 return False
221 return field.__class__ == other_field.__class__
224 class ResourceContainer(object):
225 """Container for a request body resource combined with parameters.
227 Used for API methods which may also have path or query parameters in addition
228 to a request body.
230 Attributes:
231 body_message_class: A message class to represent a request body.
232 parameters_message_class: A placeholder message class for request
233 parameters.
236 __remote_info_cache = {}
238 __combined_message_class = None
240 def __init__(self, _body_message_class=message_types.VoidMessage, **kwargs):
241 """Constructor for ResourceContainer.
243 Stores a request body message class and attempts to create one from the
244 keyword arguments passed in.
246 Args:
247 _body_message_class: A keyword argument to be treated like a positional
248 argument. This will not conflict with the potential names of fields
249 since they can't begin with underscore. We make this a keyword
250 argument since the default VoidMessage is a very common choice given
251 the prevalence of GET methods.
252 **kwargs: Keyword arguments specifying field names (the named arguments)
253 and instances of ProtoRPC fields as the values.
255 self.body_message_class = _body_message_class
256 self.parameters_message_class = type('ParameterContainer',
257 (messages.Message,), kwargs)
259 @property
260 def combined_message_class(self):
261 """A ProtoRPC message class with both request and parameters fields.
263 Caches the result in a local private variable. Uses _CopyField to create
264 copies of the fields from the existing request and parameters classes since
265 those fields are "owned" by the message classes.
267 Raises:
268 TypeError: If a field name is used in both the request message and the
269 parameters but the two fields do not represent the same type.
271 Returns:
272 Value of combined message class for this property.
274 if self.__combined_message_class is not None:
275 return self.__combined_message_class
277 fields = {}
285 field_number = 1
286 for field in self.body_message_class.all_fields():
287 fields[field.name] = _CopyField(field, number=field_number)
288 field_number += 1
289 for field in self.parameters_message_class.all_fields():
290 if field.name in fields:
291 if not _CompareFields(field, fields[field.name]):
292 raise TypeError('Field %r contained in both parameters and request '
293 'body, but the fields differ.' % (field.name,))
294 else:
296 continue
297 fields[field.name] = _CopyField(field, number=field_number)
298 field_number += 1
300 self.__combined_message_class = type('CombinedContainer',
301 (messages.Message,), fields)
302 return self.__combined_message_class
304 @classmethod
305 def add_to_cache(cls, remote_info, container):
306 """Adds a ResourceContainer to a cache tying it to a protorpc method.
308 Args:
309 remote_info: Instance of protorpc.remote._RemoteMethodInfo corresponding
310 to a method.
311 container: An instance of ResourceContainer.
313 Raises:
314 TypeError: if the container is not an instance of cls.
315 KeyError: if the remote method has been reference by a container before.
316 This created remote method should never occur because a remote method
317 is created once.
319 if not isinstance(container, cls):
320 raise TypeError('%r not an instance of %r, could not be added to cache.' %
321 (container, cls))
322 if remote_info in cls.__remote_info_cache:
323 raise KeyError('Cache has collision but should not.')
324 cls.__remote_info_cache[remote_info] = container
326 @classmethod
327 def get_request_message(cls, remote_info):
328 """Gets request message or container from remote info.
330 Args:
331 remote_info: Instance of protorpc.remote._RemoteMethodInfo corresponding
332 to a method.
334 Returns:
335 Either an instance of the request type from the remote or the
336 ResourceContainer that was cached with the remote method.
338 if remote_info in cls.__remote_info_cache:
339 return cls.__remote_info_cache[remote_info]
340 else:
341 return remote_info.request_type()
344 def _CheckListType(settings, allowed_type, name, allow_none=True):
345 """Verify that settings in list are of the allowed type or raise TypeError.
347 Args:
348 settings: The list of settings to check.
349 allowed_type: The allowed type of items in 'settings'.
350 name: Name of the setting, added to the exception.
351 allow_none: If set, None is also allowed.
353 Raises:
354 TypeError: if setting is not of the allowed type.
356 Returns:
357 The list of settings, for convenient use in assignment.
359 if settings is None:
360 if not allow_none:
361 raise TypeError('%s is None, which is not allowed.' % name)
362 return settings
363 if not isinstance(settings, (tuple, list)):
364 raise TypeError('%s is not a list.' % name)
365 if not all(isinstance(i, allowed_type) for i in settings):
366 type_list = list(set(type(setting) for setting in settings))
367 raise TypeError('%s contains types that don\'t match %s: %s' %
368 (name, allowed_type.__name__, type_list))
369 return settings
372 def _CheckType(value, check_type, name, allow_none=True):
373 """Check that the type of an object is acceptable.
375 Args:
376 value: The object whose type is to be checked.
377 check_type: The type that the object must be an instance of.
378 name: Name of the object, to be placed in any error messages.
379 allow_none: True if value can be None, false if not.
381 Raises:
382 TypeError: If value is not an acceptable type.
384 if value is None and allow_none:
385 return
386 if not isinstance(value, check_type):
387 raise TypeError('%s type doesn\'t match %s.' % (name, check_type))
390 def _CheckEnum(value, check_type, name):
391 if value is None:
392 return
393 if value not in check_type.reverse_mapping:
394 raise TypeError('%s is not a valid value for %s' % (value, name))
398 class _ApiInfo(object):
399 """Configurable attributes of an API.
401 A structured data object used to store API information associated with each
402 remote.Service-derived class that implements an API. This stores properties
403 that could be different for each class (such as the path or
404 collection/resource name), as well as properties common to all classes in
405 the API (such as API name and version).
408 @util.positional(2)
409 def __init__(self, common_info, resource_name=None, path=None, audiences=None,
410 scopes=None, allowed_client_ids=None, auth_level=None):
411 """Constructor for _ApiInfo.
413 Args:
414 common_info: _ApiDecorator.__ApiCommonInfo, Information that's common for
415 all classes that implement an API.
416 resource_name: string, The collection that the annotated class will
417 implement in the API. (Default: None)
418 path: string, Base request path for all methods in this API.
419 (Default: None)
420 audiences: list of strings, Acceptable audiences for authentication.
421 (Default: None)
422 scopes: list of strings, Acceptable scopes for authentication.
423 (Default: None)
424 allowed_client_ids: list of strings, Acceptable client IDs for auth.
425 (Default: None)
426 auth_level: enum from AUTH_LEVEL, Frontend authentication level.
427 (Default: None)
429 _CheckType(resource_name, basestring, 'resource_name')
430 _CheckType(path, basestring, 'path')
431 _CheckListType(audiences, basestring, 'audiences')
432 _CheckListType(scopes, basestring, 'scopes')
433 _CheckListType(allowed_client_ids, basestring, 'allowed_client_ids')
434 _CheckEnum(auth_level, AUTH_LEVEL, 'auth_level')
436 self.__common_info = common_info
437 self.__resource_name = resource_name
438 self.__path = path
439 self.__audiences = audiences
440 self.__scopes = scopes
441 self.__allowed_client_ids = allowed_client_ids
442 self.__auth_level = auth_level
444 def is_same_api(self, other):
445 """Check if this implements the same API as another _ApiInfo instance."""
446 if not isinstance(other, _ApiInfo):
447 return False
449 return self.__common_info is other.__common_info
451 @property
452 def name(self):
453 """Name of the API."""
454 return self.__common_info.name
456 @property
457 def version(self):
458 """Version of the API."""
459 return self.__common_info.version
461 @property
462 def description(self):
463 """Description of the API."""
464 return self.__common_info.description
466 @property
467 def hostname(self):
468 """Hostname for the API."""
469 return self.__common_info.hostname
471 @property
472 def audiences(self):
473 """List of audiences accepted for the API, overriding the defaults."""
474 if self.__audiences is not None:
475 return self.__audiences
476 return self.__common_info.audiences
478 @property
479 def scopes(self):
480 """List of scopes accepted for the API, overriding the defaults."""
481 if self.__scopes is not None:
482 return self.__scopes
483 return self.__common_info.scopes
485 @property
486 def allowed_client_ids(self):
487 """List of client IDs accepted for the API, overriding the defaults."""
488 if self.__allowed_client_ids is not None:
489 return self.__allowed_client_ids
490 return self.__common_info.allowed_client_ids
492 @property
493 def auth_level(self):
494 """Enum from AUTH_LEVEL specifying the frontend authentication level."""
495 if self.__auth_level is not None:
496 return self.__auth_level
497 return self.__common_info.auth_level
499 @property
500 def canonical_name(self):
501 """Canonical name for the API."""
502 return self.__common_info.canonical_name
504 @property
505 def auth(self):
506 """Authentication configuration information for this API."""
507 return self.__common_info.auth
509 @property
510 def owner_domain(self):
511 """Domain of the owner of this API."""
512 return self.__common_info.owner_domain
514 @property
515 def owner_name(self):
516 """Name of the owner of this API."""
517 return self.__common_info.owner_name
519 @property
520 def package_path(self):
521 """Package this API belongs to, '/' delimited. Used by client libs."""
522 return self.__common_info.package_path
524 @property
525 def frontend_limits(self):
526 """Optional query limits for unregistered developers."""
527 return self.__common_info.frontend_limits
529 @property
530 def title(self):
531 """Human readable name of this API."""
532 return self.__common_info.title
534 @property
535 def documentation(self):
536 """Link to the documentation for this version of the API."""
537 return self.__common_info.documentation
539 @property
540 def resource_name(self):
541 """Resource name for the class this decorates."""
542 return self.__resource_name
544 @property
545 def path(self):
546 """Base path prepended to any method paths in the class this decorates."""
547 return self.__path
550 class _ApiDecorator(object):
551 """Decorator for single- or multi-class APIs.
553 An instance of this class can be used directly as a decorator for a
554 single-class API. Or call the api_class() method to decorate a multi-class
555 API.
558 @util.positional(3)
559 def __init__(self, name, version, description=None, hostname=None,
560 audiences=None, scopes=None, allowed_client_ids=None,
561 canonical_name=None, auth=None, owner_domain=None,
562 owner_name=None, package_path=None, frontend_limits=None,
563 title=None, documentation=None, auth_level=None):
564 """Constructor for _ApiDecorator.
566 Args:
567 name: string, Name of the API.
568 version: string, Version of the API.
569 description: string, Short description of the API (Default: None)
570 hostname: string, Hostname of the API (Default: app engine default host)
571 audiences: list of strings, Acceptable audiences for authentication.
572 scopes: list of strings, Acceptable scopes for authentication.
573 allowed_client_ids: list of strings, Acceptable client IDs for auth.
574 canonical_name: string, the canonical name for the API, a more human
575 readable version of the name.
576 auth: ApiAuth instance, the authentication configuration information
577 for this API.
578 owner_domain: string, the domain of the person or company that owns
579 this API. Along with owner_name, this provides hints to properly
580 name client libraries for this API.
581 owner_name: string, the name of the owner of this API. Along with
582 owner_domain, this provides hints to properly name client libraries
583 for this API.
584 package_path: string, the "package" this API belongs to. This '/'
585 delimited value specifies logical groupings of APIs. This is used by
586 client libraries of this API.
587 frontend_limits: ApiFrontEndLimits, optional query limits for unregistered
588 developers.
589 title: string, the human readable title of your API. It is exposed in the
590 discovery service.
591 documentation: string, a URL where users can find documentation about this
592 version of the API. This will be surfaced in the API Explorer and GPE
593 plugin to allow users to learn about your service.
594 auth_level: enum from AUTH_LEVEL, Frontend authentication level.
596 self.__common_info = self.__ApiCommonInfo(
597 name, version, description=description, hostname=hostname,
598 audiences=audiences, scopes=scopes,
599 allowed_client_ids=allowed_client_ids,
600 canonical_name=canonical_name, auth=auth, owner_domain=owner_domain,
601 owner_name=owner_name, package_path=package_path,
602 frontend_limits=frontend_limits, title=title,
603 documentation=documentation, auth_level=auth_level)
604 self.__classes = []
606 class __ApiCommonInfo(object):
607 """API information that's common among all classes that implement an API.
609 When a remote.Service-derived class implements part of an API, there is
610 some common information that remains constant across all such classes
611 that implement the same API. This includes things like name, version,
612 hostname, and so on. __ApiComminInfo stores that common information, and
613 a single __ApiCommonInfo instance is shared among all classes that
614 implement the same API, guaranteeing that they share the same common
615 information.
617 Some of these values can be overridden (such as audiences and scopes),
618 while some can't and remain the same for all classes that implement
619 the API (such as name and version).
622 @util.positional(3)
623 def __init__(self, name, version, description=None, hostname=None,
624 audiences=None, scopes=None, allowed_client_ids=None,
625 canonical_name=None, auth=None, owner_domain=None,
626 owner_name=None, package_path=None, frontend_limits=None,
627 title=None, documentation=None, auth_level=None):
628 """Constructor for _ApiCommonInfo.
630 Args:
631 name: string, Name of the API.
632 version: string, Version of the API.
633 description: string, Short description of the API (Default: None)
634 hostname: string, Hostname of the API (Default: app engine default host)
635 audiences: list of strings, Acceptable audiences for authentication.
636 scopes: list of strings, Acceptable scopes for authentication.
637 allowed_client_ids: list of strings, Acceptable client IDs for auth.
638 canonical_name: string, the canonical name for the API, a more human
639 readable version of the name.
640 auth: ApiAuth instance, the authentication configuration information
641 for this API.
642 owner_domain: string, the domain of the person or company that owns
643 this API. Along with owner_name, this provides hints to properly
644 name client libraries for this API.
645 owner_name: string, the name of the owner of this API. Along with
646 owner_domain, this provides hints to properly name client libraries
647 for this API.
648 package_path: string, the "package" this API belongs to. This '/'
649 delimited value specifies logical groupings of APIs. This is used by
650 client libraries of this API.
651 frontend_limits: ApiFrontEndLimits, optional query limits for
652 unregistered developers.
653 title: string, the human readable title of your API. It is exposed in
654 the discovery service.
655 documentation: string, a URL where users can find documentation about
656 this version of the API. This will be surfaced in the API Explorer and
657 GPE plugin to allow users to learn about your service.
658 auth_level: enum from AUTH_LEVEL, Frontend authentication level.
660 _CheckType(name, basestring, 'name', allow_none=False)
661 _CheckType(version, basestring, 'version', allow_none=False)
662 _CheckType(description, basestring, 'description')
663 _CheckType(hostname, basestring, 'hostname')
664 _CheckListType(audiences, basestring, 'audiences')
665 _CheckListType(scopes, basestring, 'scopes')
666 _CheckListType(allowed_client_ids, basestring, 'allowed_client_ids')
667 _CheckType(canonical_name, basestring, 'canonical_name')
668 _CheckType(auth, ApiAuth, 'auth')
669 _CheckType(owner_domain, basestring, 'owner_domain')
670 _CheckType(owner_name, basestring, 'owner_name')
671 _CheckType(package_path, basestring, 'package_path')
672 _CheckType(frontend_limits, ApiFrontEndLimits, 'frontend_limits')
673 _CheckType(title, basestring, 'title')
674 _CheckType(documentation, basestring, 'documentation')
675 _CheckEnum(auth_level, AUTH_LEVEL, 'auth_level')
677 if hostname is None:
678 hostname = app_identity.get_default_version_hostname()
679 if audiences is None:
680 audiences = []
681 if scopes is None:
682 scopes = [EMAIL_SCOPE]
683 if allowed_client_ids is None:
684 allowed_client_ids = [API_EXPLORER_CLIENT_ID]
685 if auth_level is None:
686 auth_level = AUTH_LEVEL.NONE
688 self.__name = name
689 self.__version = version
690 self.__description = description
691 self.__hostname = hostname
692 self.__audiences = audiences
693 self.__scopes = scopes
694 self.__allowed_client_ids = allowed_client_ids
695 self.__canonical_name = canonical_name
696 self.__auth = auth
697 self.__owner_domain = owner_domain
698 self.__owner_name = owner_name
699 self.__package_path = package_path
700 self.__frontend_limits = frontend_limits
701 self.__title = title
702 self.__documentation = documentation
703 self.__auth_level = auth_level
705 @property
706 def name(self):
707 """Name of the API."""
708 return self.__name
710 @property
711 def version(self):
712 """Version of the API."""
713 return self.__version
715 @property
716 def description(self):
717 """Description of the API."""
718 return self.__description
720 @property
721 def hostname(self):
722 """Hostname for the API."""
723 return self.__hostname
725 @property
726 def audiences(self):
727 """List of audiences accepted by default for the API."""
728 return self.__audiences
730 @property
731 def scopes(self):
732 """List of scopes accepted by default for the API."""
733 return self.__scopes
735 @property
736 def allowed_client_ids(self):
737 """List of client IDs accepted by default for the API."""
738 return self.__allowed_client_ids
740 @property
741 def auth_level(self):
742 """Enum from AUTH_LEVEL specifying default frontend auth level."""
743 return self.__auth_level
745 @property
746 def canonical_name(self):
747 """Canonical name for the API."""
748 return self.__canonical_name
750 @property
751 def auth(self):
752 """Authentication configuration for this API."""
753 return self.__auth
755 @property
756 def owner_domain(self):
757 """Domain of the owner of this API."""
758 return self.__owner_domain
760 @property
761 def owner_name(self):
762 """Name of the owner of this API."""
763 return self.__owner_name
765 @property
766 def package_path(self):
767 """Package this API belongs to, '/' delimited. Used by client libs."""
768 return self.__package_path
770 @property
771 def frontend_limits(self):
772 """Optional query limits for unregistered developers."""
773 return self.__frontend_limits
775 @property
776 def title(self):
777 """Human readable name of this API."""
778 return self.__title
780 @property
781 def documentation(self):
782 """Link to the documentation for this version of the API."""
783 return self.__documentation
785 def __call__(self, service_class):
786 """Decorator for ProtoRPC class that configures Google's API server.
788 Args:
789 service_class: remote.Service class, ProtoRPC service class being wrapped.
791 Returns:
792 Same class with API attributes assigned in api_info.
794 return self.api_class()(service_class)
796 def api_class(self, resource_name=None, path=None, audiences=None,
797 scopes=None, allowed_client_ids=None, auth_level=None):
798 """Get a decorator for a class that implements an API.
800 This can be used for single-class or multi-class implementations. It's
801 used implicitly in simple single-class APIs that only use @api directly.
803 Args:
804 resource_name: string, Resource name for the class this decorates.
805 (Default: None)
806 path: string, Base path prepended to any method paths in the class this
807 decorates. (Default: None)
808 audiences: list of strings, Acceptable audiences for authentication.
809 (Default: None)
810 scopes: list of strings, Acceptable scopes for authentication.
811 (Default: None)
812 allowed_client_ids: list of strings, Acceptable client IDs for auth.
813 (Default: None)
814 auth_level: enum from AUTH_LEVEL, Frontend authentication level.
815 (Default: None)
817 Returns:
818 A decorator function to decorate a class that implements an API.
821 def apiserving_api_decorator(api_class):
822 """Decorator for ProtoRPC class that configures Google's API server.
824 Args:
825 api_class: remote.Service class, ProtoRPC service class being wrapped.
827 Returns:
828 Same class with API attributes assigned in api_info.
830 self.__classes.append(api_class)
831 api_class.api_info = _ApiInfo(
832 self.__common_info, resource_name=resource_name,
833 path=path, audiences=audiences, scopes=scopes,
834 allowed_client_ids=allowed_client_ids, auth_level=auth_level)
835 return api_class
837 return apiserving_api_decorator
839 def get_api_classes(self):
840 """Get the list of remote.Service classes that implement this API."""
841 return self.__classes
844 class ApiAuth(object):
845 """Optional authorization configuration information for an API."""
847 def __init__(self, allow_cookie_auth=None, blocked_regions=None):
848 """Constructor for ApiAuth, authentication information for an API.
850 Args:
851 allow_cookie_auth: boolean, whether cooking auth is allowed. By
852 default, API methods do not allow cookie authentication, and
853 require the use of OAuth2 or ID tokens. Setting this field to
854 True will allow cookies to be used to access the API, with
855 potentially dangerous results. Please be very cautious in enabling
856 this setting, and make sure to require appropriate XSRF tokens to
857 protect your API.
858 blocked_regions: list of Strings, a list of 2-letter ISO region codes
859 to block.
861 _CheckType(allow_cookie_auth, bool, 'allow_cookie_auth')
862 _CheckListType(blocked_regions, basestring, 'blocked_regions')
864 self.__allow_cookie_auth = allow_cookie_auth
865 self.__blocked_regions = blocked_regions
867 @property
868 def allow_cookie_auth(self):
869 """Whether cookie authentication is allowed for this API."""
870 return self.__allow_cookie_auth
872 @property
873 def blocked_regions(self):
874 """List of 2-letter ISO region codes to block."""
875 return self.__blocked_regions
878 class ApiFrontEndLimitRule(object):
879 """Custom rule to limit unregistered traffic."""
881 def __init__(self, match=None, qps=None, user_qps=None, daily=None,
882 analytics_id=None):
883 """Constructor for ApiFrontEndLimitRule.
885 Args:
886 match: string, the matching rule that defines this traffic segment.
887 qps: int, the aggregate QPS for this segment.
888 user_qps: int, the per-end-user QPS for this segment.
889 daily: int, the aggregate daily maximum for this segment.
890 analytics_id: string, the project ID under which traffic for this segment
891 will be logged.
893 _CheckType(match, basestring, 'match')
894 _CheckType(qps, int, 'qps')
895 _CheckType(user_qps, int, 'user_qps')
896 _CheckType(daily, int, 'daily')
897 _CheckType(analytics_id, basestring, 'analytics_id')
899 self.__match = match
900 self.__qps = qps
901 self.__user_qps = user_qps
902 self.__daily = daily
903 self.__analytics_id = analytics_id
905 @property
906 def match(self):
907 """The matching rule that defines this traffic segment."""
908 return self.__match
910 @property
911 def qps(self):
912 """The aggregate QPS for this segment."""
913 return self.__qps
915 @property
916 def user_qps(self):
917 """The per-end-user QPS for this segment."""
918 return self.__user_qps
920 @property
921 def daily(self):
922 """The aggregate daily maximum for this segment."""
923 return self.__daily
925 @property
926 def analytics_id(self):
927 """Project ID under which traffic for this segment will be logged."""
928 return self.__analytics_id
931 class ApiFrontEndLimits(object):
932 """Optional front end limit information for an API."""
934 def __init__(self, unregistered_user_qps=None, unregistered_qps=None,
935 unregistered_daily=None, rules=None):
936 """Constructor for ApiFrontEndLimits, front end limit info for an API.
938 Args:
939 unregistered_user_qps: int, the per-end-user QPS. Users are identified
940 by their IP address. A value of 0 will block unregistered requests.
941 unregistered_qps: int, an aggregate QPS upper-bound for all unregistered
942 traffic. A value of 0 currently means unlimited, though it might change
943 in the future. To block unregistered requests, use unregistered_user_qps
944 or unregistered_daily instead.
945 unregistered_daily: int, an aggregate daily upper-bound for all
946 unregistered traffic. A value of 0 will block unregistered requests.
947 rules: A list or tuple of ApiFrontEndLimitRule instances: custom rules
948 used to apply limits to unregistered traffic.
950 _CheckType(unregistered_user_qps, int, 'unregistered_user_qps')
951 _CheckType(unregistered_qps, int, 'unregistered_qps')
952 _CheckType(unregistered_daily, int, 'unregistered_daily')
953 _CheckListType(rules, ApiFrontEndLimitRule, 'rules')
955 self.__unregistered_user_qps = unregistered_user_qps
956 self.__unregistered_qps = unregistered_qps
957 self.__unregistered_daily = unregistered_daily
958 self.__rules = rules
960 @property
961 def unregistered_user_qps(self):
962 """Per-end-user QPS limit."""
963 return self.__unregistered_user_qps
965 @property
966 def unregistered_qps(self):
967 """Aggregate QPS upper-bound for all unregistered traffic."""
968 return self.__unregistered_qps
970 @property
971 def unregistered_daily(self):
972 """Aggregate daily upper-bound for all unregistered traffic."""
973 return self.__unregistered_daily
975 @property
976 def rules(self):
977 """Custom rules used to apply limits to unregistered traffic."""
978 return self.__rules
981 @util.positional(2)
982 def api(name, version, description=None, hostname=None, audiences=None,
983 scopes=None, allowed_client_ids=None, canonical_name=None,
984 auth=None, owner_domain=None, owner_name=None, package_path=None,
985 frontend_limits=None, title=None, documentation=None, auth_level=None):
986 """Decorate a ProtoRPC Service class for use by the framework above.
988 This decorator can be used to specify an API name, version, description, and
989 hostname for your API.
991 Sample usage (python 2.7):
992 @endpoints.api(name='guestbook', version='v0.2',
993 description='Guestbook API')
994 class PostService(remote.Service):
997 Sample usage (python 2.5):
998 class PostService(remote.Service):
1000 endpoints.api(name='guestbook', version='v0.2',
1001 description='Guestbook API')(PostService)
1003 Sample usage if multiple classes implement one API:
1004 api_root = endpoints.api(name='library', version='v1.0')
1006 @api_root.api_class(resource_name='shelves')
1007 class Shelves(remote.Service):
1010 @api_root.api_class(resource_name='books', path='books')
1011 class Books(remote.Service):
1014 Args:
1015 name: string, Name of the API.
1016 version: string, Version of the API.
1017 description: string, Short description of the API (Default: None)
1018 hostname: string, Hostname of the API (Default: app engine default host)
1019 audiences: list of strings, Acceptable audiences for authentication.
1020 scopes: list of strings, Acceptable scopes for authentication.
1021 allowed_client_ids: list of strings, Acceptable client IDs for auth.
1022 canonical_name: string, the canonical name for the API, a more human
1023 readable version of the name.
1024 auth: ApiAuth instance, the authentication configuration information
1025 for this API.
1026 owner_domain: string, the domain of the person or company that owns
1027 this API. Along with owner_name, this provides hints to properly
1028 name client libraries for this API.
1029 owner_name: string, the name of the owner of this API. Along with
1030 owner_domain, this provides hints to properly name client libraries
1031 for this API.
1032 package_path: string, the "package" this API belongs to. This '/'
1033 delimited value specifies logical groupings of APIs. This is used by
1034 client libraries of this API.
1035 frontend_limits: ApiFrontEndLimits, optional query limits for unregistered
1036 developers.
1037 title: string, the human readable title of your API. It is exposed in the
1038 discovery service.
1039 documentation: string, a URL where users can find documentation about this
1040 version of the API. This will be surfaced in the API Explorer and GPE
1041 plugin to allow users to learn about your service.
1042 auth_level: enum from AUTH_LEVEL, frontend authentication level.
1044 Returns:
1045 Class decorated with api_info attribute, an instance of ApiInfo.
1048 return _ApiDecorator(name, version, description=description,
1049 hostname=hostname, audiences=audiences, scopes=scopes,
1050 allowed_client_ids=allowed_client_ids,
1051 canonical_name=canonical_name, auth=auth,
1052 owner_domain=owner_domain, owner_name=owner_name,
1053 package_path=package_path,
1054 frontend_limits=frontend_limits, title=title,
1055 documentation=documentation, auth_level=auth_level)
1058 class CacheControl(object):
1059 """Cache control settings for an API method.
1061 Setting is composed of a directive and maximum cache age.
1062 Available types:
1063 PUBLIC - Allows clients and proxies to cache responses.
1064 PRIVATE - Allows only clients to cache responses.
1065 NO_CACHE - Allows none to cache responses.
1067 PUBLIC = 'public'
1068 PRIVATE = 'private'
1069 NO_CACHE = 'no-cache'
1070 VALID_VALUES = (PUBLIC, PRIVATE, NO_CACHE)
1072 def __init__(self, directive=NO_CACHE, max_age_seconds=0):
1073 """Constructor.
1075 Args:
1076 directive: string, Cache control directive, as above. (Default: NO_CACHE)
1077 max_age_seconds: int, Maximum age of cache responses. (Default: 0)
1079 if directive not in self.VALID_VALUES:
1080 directive = self.NO_CACHE
1081 self.__directive = directive
1082 self.__max_age_seconds = max_age_seconds
1084 @property
1085 def directive(self):
1086 """The cache setting for this method, PUBLIC, PRIVATE, or NO_CACHE."""
1087 return self.__directive
1089 @property
1090 def max_age_seconds(self):
1091 """The maximum age of cache responses for this method, in seconds."""
1092 return self.__max_age_seconds
1095 class _MethodInfo(object):
1096 """Configurable attributes of an API method.
1098 Consolidates settings from @method decorator and/or any settings that were
1099 calculating from the ProtoRPC method name, so they only need to be calculated
1100 once.
1103 @util.positional(1)
1104 def __init__(self, name=None, path=None, http_method=None,
1105 cache_control=None, scopes=None, audiences=None,
1106 allowed_client_ids=None, auth_level=None):
1107 """Constructor.
1109 Args:
1110 name: string, Name of the method, prepended with <apiname>. to make it
1111 unique.
1112 path: string, Path portion of the URL to the method, for RESTful methods.
1113 http_method: string, HTTP method supported by the method.
1114 cache_control: CacheControl, Cache settings for the API method.
1115 scopes: list of string, OAuth2 token must contain one of these scopes.
1116 audiences: list of string, IdToken must contain one of these audiences.
1117 allowed_client_ids: list of string, Client IDs allowed to call the method.
1118 auth_level: enum from AUTH_LEVEL, Frontend auth level for the method.
1120 self.__name = name
1121 self.__path = path
1122 self.__http_method = http_method
1123 self.__cache_control = cache_control
1124 self.__scopes = scopes
1125 self.__audiences = audiences
1126 self.__allowed_client_ids = allowed_client_ids
1127 self.__auth_level = auth_level
1129 def __safe_name(self, method_name):
1130 """Restrict method name to a-zA-Z0-9_, first char lowercase."""
1133 safe_name = re.sub('[^\.a-zA-Z0-9_]', '', method_name)
1136 safe_name = safe_name.lstrip('_')
1140 return safe_name[0:1].lower() + safe_name[1:]
1142 @property
1143 def name(self):
1144 """Method name as specified in decorator or derived."""
1145 return self.__name
1147 def get_path(self, api_info):
1148 """Get the path portion of the URL to the method (for RESTful methods).
1150 Request path can be specified in the method, and it could have a base
1151 path prepended to it.
1153 Args:
1154 api_info: API information for this API, possibly including a base path.
1155 This is the api_info property on the class that's been annotated for
1156 this API.
1158 Returns:
1159 This method's request path (not including the http://.../_ah/api/ prefix).
1161 Raises:
1162 ApiConfigurationError: If the path isn't properly formatted.
1164 path = self.__path or ''
1165 if path and path[0] == '/':
1167 path = path[1:]
1168 else:
1170 if api_info.path:
1171 path = '%s%s%s' % (api_info.path, '/' if path else '', path)
1174 for part in path.split('/'):
1175 if part and '{' in part and '}' in part:
1176 if re.match('^{[^{}]+}$', part) is None:
1177 raise ApiConfigurationError('Invalid path segment: %s (part of %s)' %
1178 (part, path))
1179 return path
1181 @property
1182 def http_method(self):
1183 """HTTP method supported by the method (e.g. GET, POST)."""
1184 return self.__http_method
1186 @property
1187 def cache_control(self):
1188 """Cache control setting for the API method."""
1189 return self.__cache_control
1191 @property
1192 def scopes(self):
1193 """List of scopes for the API method."""
1194 return self.__scopes
1196 @property
1197 def audiences(self):
1198 """List of audiences for the API method."""
1199 return self.__audiences
1201 @property
1202 def allowed_client_ids(self):
1203 """List of allowed client IDs for the API method."""
1204 return self.__allowed_client_ids
1206 @property
1207 def auth_level(self):
1208 """Enum from AUTH_LEVEL specifying default frontend auth level."""
1209 return self.__auth_level
1211 def method_id(self, api_info):
1212 """Computed method name."""
1216 if api_info.resource_name:
1217 resource_part = '.%s' % self.__safe_name(api_info.resource_name)
1218 else:
1219 resource_part = ''
1220 return '%s%s.%s' % (self.__safe_name(api_info.name), resource_part,
1221 self.__safe_name(self.name))
1224 @util.positional(2)
1225 def method(request_message=message_types.VoidMessage,
1226 response_message=message_types.VoidMessage,
1227 name=None,
1228 path=None,
1229 http_method='POST',
1230 cache_control=None,
1231 scopes=None,
1232 audiences=None,
1233 allowed_client_ids=None,
1234 auth_level=None):
1235 """Decorate a ProtoRPC Method for use by the framework above.
1237 This decorator can be used to specify a method name, path, http method,
1238 cache control, scopes, audiences, client ids and auth_level.
1240 Sample usage:
1241 @api_config.method(RequestMessage, ResponseMessage,
1242 name='insert', http_method='PUT')
1243 def greeting_insert(request):
1245 return response
1247 Args:
1248 request_message: Message type of expected request.
1249 response_message: Message type of expected response.
1250 name: string, Name of the method, prepended with <apiname>. to make it
1251 unique. (Default: python method name)
1252 path: string, Path portion of the URL to the method, for RESTful methods.
1253 http_method: string, HTTP method supported by the method. (Default: POST)
1254 cache_control: CacheControl, Cache settings for the API method.
1255 scopes: list of string, OAuth2 token must contain one of these scopes.
1256 audiences: list of string, IdToken must contain one of these audiences.
1257 allowed_client_ids: list of string, Client IDs allowed to call the method.
1258 Currently limited to 5. If None, no calls will be allowed.
1259 auth_level: enum from AUTH_LEVEL, Frontend auth level for the method.
1261 Returns:
1262 'apiserving_method_wrapper' function.
1264 Raises:
1265 ValueError: if more than 5 allowed_client_ids are specified.
1266 TypeError: if the request_type or response_type parameters are not
1267 proper subclasses of messages.Message.
1271 DEFAULT_HTTP_METHOD = 'POST'
1273 def check_type(setting, allowed_type, name, allow_none=True):
1274 """Verify that the setting is of the allowed type or raise TypeError.
1276 Args:
1277 setting: The setting to check.
1278 allowed_type: The allowed type.
1279 name: Name of the setting, added to the exception.
1280 allow_none: If set, None is also allowed.
1282 Raises:
1283 TypeError: if setting is not of the allowed type.
1285 Returns:
1286 The setting, for convenient use in assignment.
1288 if (setting is None and allow_none or
1289 isinstance(setting, allowed_type)):
1290 return setting
1291 raise TypeError('%s is not of type %s' % (name, allowed_type.__name__))
1293 def apiserving_method_decorator(api_method):
1294 """Decorator for ProtoRPC method that configures Google's API server.
1296 Args:
1297 api_method: Original method being wrapped.
1299 Returns:
1300 Function responsible for actual invocation.
1301 Assigns the following attributes to invocation function:
1302 remote: Instance of RemoteInfo, contains remote method information.
1303 remote.request_type: Expected request type for remote method.
1304 remote.response_type: Response type returned from remote method.
1305 method_info: Instance of _MethodInfo, api method configuration.
1306 It is also assigned attributes corresponding to the aforementioned kwargs.
1308 Raises:
1309 TypeError: if the request_type or response_type parameters are not
1310 proper subclasses of messages.Message.
1311 KeyError: if the request_message is a ResourceContainer and the newly
1312 created remote method has been reference by the container before. This
1313 should never occur because a remote method is created once.
1315 if isinstance(request_message, ResourceContainer):
1316 remote_decorator = remote.method(request_message.combined_message_class,
1317 response_message)
1318 else:
1319 remote_decorator = remote.method(request_message, response_message)
1320 remote_method = remote_decorator(api_method)
1322 def invoke_remote(service_instance, request):
1325 users_id_token._maybe_set_current_user_vars(
1326 invoke_remote, api_info=getattr(service_instance, 'api_info', None),
1327 request=request)
1329 return remote_method(service_instance, request)
1331 invoke_remote.remote = remote_method.remote
1332 if isinstance(request_message, ResourceContainer):
1333 ResourceContainer.add_to_cache(invoke_remote.remote, request_message)
1335 invoke_remote.method_info = _MethodInfo(
1336 name=name or api_method.__name__, path=path or api_method.__name__,
1337 http_method=http_method or DEFAULT_HTTP_METHOD,
1338 cache_control=cache_control, scopes=scopes, audiences=audiences,
1339 allowed_client_ids=allowed_client_ids, auth_level=auth_level)
1340 invoke_remote.__name__ = invoke_remote.method_info.name
1341 return invoke_remote
1343 check_type(cache_control, CacheControl, 'cache_control')
1344 _CheckListType(scopes, basestring, 'scopes')
1345 _CheckListType(audiences, basestring, 'audiences')
1346 _CheckListType(allowed_client_ids, basestring, 'allowed_client_ids')
1347 _CheckEnum(auth_level, AUTH_LEVEL, 'auth_level')
1348 return apiserving_method_decorator
1351 class ApiConfigGenerator(object):
1352 """Generates an API configuration from a ProtoRPC service.
1354 Example:
1356 class HelloRequest(messages.Message):
1357 my_name = messages.StringField(1, required=True)
1359 class HelloResponse(messages.Message):
1360 hello = messages.StringField(1, required=True)
1362 class HelloService(remote.Service):
1364 @remote.method(HelloRequest, HelloResponse)
1365 def hello(self, request):
1366 return HelloResponse(hello='Hello there, %s!' %
1367 request.my_name)
1369 api_config = ApiConfigGenerator().pretty_print_config_to_json(HelloService)
1371 The resulting api_config will be a JSON document describing the API
1372 implemented by HelloService.
1378 __NO_BODY = 1
1379 __HAS_BODY = 2
1381 def __init__(self):
1382 self.__parser = message_parser.MessageTypeToJsonSchema()
1385 self.__request_schema = {}
1388 self.__response_schema = {}
1391 self.__id_from_name = {}
1393 def __get_request_kind(self, method_info):
1394 """Categorize the type of the request.
1396 Args:
1397 method_info: _MethodInfo, method information.
1399 Returns:
1400 The kind of request.
1402 if method_info.http_method in ('GET', 'DELETE'):
1403 return self.__NO_BODY
1404 else:
1405 return self.__HAS_BODY
1407 def __field_to_subfields(self, field):
1408 """Fully describes data represented by field, including the nested case.
1410 In the case that the field is not a message field, we have no fields nested
1411 within a message definition, so we can simply return that field. However, in
1412 the nested case, we can't simply describe the data with one field or even
1413 with one chain of fields.
1415 For example, if we have a message field
1417 m_field = messages.MessageField(RefClass, 1)
1419 which references a class with two fields:
1421 class RefClass(messages.Message):
1422 one = messages.StringField(1)
1423 two = messages.IntegerField(2)
1425 then we would need to include both one and two to represent all the
1426 data contained.
1428 Calling __field_to_subfields(m_field) would return:
1430 [<MessageField "m_field">, <StringField "one">],
1431 [<MessageField "m_field">, <StringField "two">],
1434 If the second field was instead a message field
1436 class RefClass(messages.Message):
1437 one = messages.StringField(1)
1438 two = messages.MessageField(OtherRefClass, 2)
1440 referencing another class with two fields
1442 class OtherRefClass(messages.Message):
1443 three = messages.BooleanField(1)
1444 four = messages.FloatField(2)
1446 then we would need to recurse one level deeper for two.
1448 With this change, calling __field_to_subfields(m_field) would return:
1450 [<MessageField "m_field">, <StringField "one">],
1451 [<MessageField "m_field">, <StringField "two">, <StringField "three">],
1452 [<MessageField "m_field">, <StringField "two">, <StringField "four">],
1455 Args:
1456 field: An instance of a subclass of messages.Field.
1458 Returns:
1459 A list of lists, where each sublist is a list of fields.
1462 if not isinstance(field, messages.MessageField):
1463 return [[field]]
1465 result = []
1466 for subfield in sorted(field.message_type.all_fields(),
1467 key=lambda f: f.number):
1468 subfield_results = self.__field_to_subfields(subfield)
1469 for subfields_list in subfield_results:
1470 subfields_list.insert(0, field)
1471 result.append(subfields_list)
1472 return result
1478 def __field_to_parameter_type(self, field):
1479 """Converts the field variant type into a string describing the parameter.
1481 Args:
1482 field: An instance of a subclass of messages.Field.
1484 Returns:
1485 A string corresponding to the variant enum of the field, with a few
1486 exceptions. In the case of signed ints, the 's' is dropped; for the BOOL
1487 variant, 'boolean' is used; and for the ENUM variant, 'string' is used.
1489 Raises:
1490 TypeError: if the field variant is a message variant.
1498 variant = field.variant
1499 if variant == messages.Variant.MESSAGE:
1500 raise TypeError('A message variant can\'t be used in a parameter.')
1502 custom_variant_map = {
1503 messages.Variant.SINT32: 'int32',
1504 messages.Variant.SINT64: 'int64',
1505 messages.Variant.BOOL: 'boolean',
1506 messages.Variant.ENUM: 'string',
1508 return custom_variant_map.get(variant) or variant.name.lower()
1510 def __get_path_parameters(self, path):
1511 """Parses path paremeters from a URI path and organizes them by parameter.
1513 Some of the parameters may correspond to message fields, and so will be
1514 represented as segments corresponding to each subfield; e.g. first.second if
1515 the field "second" in the message field "first" is pulled from the path.
1517 The resulting dictionary uses the first segments as keys and each key has as
1518 value the list of full parameter values with first segment equal to the key.
1520 If the match path parameter is null, that part of the path template is
1521 ignored; this occurs if '{}' is used in a template.
1523 Args:
1524 path: String; a URI path, potentially with some parameters.
1526 Returns:
1527 A dictionary with strings as keys and list of strings as values.
1529 path_parameters_by_segment = {}
1530 for format_var_name in re.findall(_PATH_VARIABLE_PATTERN, path):
1531 first_segment = format_var_name.split('.', 1)[0]
1532 matches = path_parameters_by_segment.setdefault(first_segment, [])
1533 matches.append(format_var_name)
1535 return path_parameters_by_segment
1537 def __validate_simple_subfield(self, parameter, field, segment_list,
1538 _segment_index=0):
1539 """Verifies that a proposed subfield actually exists and is a simple field.
1541 Here, simple means it is not a MessageField (nested).
1543 Args:
1544 parameter: String; the '.' delimited name of the current field being
1545 considered. This is relative to some root.
1546 field: An instance of a subclass of messages.Field. Corresponds to the
1547 previous segment in the path (previous relative to _segment_index),
1548 since this field should be a message field with the current segment
1549 as a field in the message class.
1550 segment_list: The full list of segments from the '.' delimited subfield
1551 being validated.
1552 _segment_index: Integer; used to hold the position of current segment so
1553 that segment_list can be passed as a reference instead of having to
1554 copy using segment_list[1:] at each step.
1556 Raises:
1557 TypeError: If the final subfield (indicated by _segment_index relative
1558 to the length of segment_list) is a MessageField.
1559 TypeError: If at any stage the lookup at a segment fails, e.g if a.b
1560 exists but a.b.c does not exist. This can happen either if a.b is not
1561 a message field or if a.b.c is not a property on the message class from
1562 a.b.
1564 if _segment_index >= len(segment_list):
1566 if isinstance(field, messages.MessageField):
1567 field_class = field.__class__.__name__
1568 raise TypeError('Can\'t use messages in path. Subfield %r was '
1569 'included but is a %s.' % (parameter, field_class))
1570 return
1572 segment = segment_list[_segment_index]
1573 parameter += '.' + segment
1574 try:
1575 field = field.type.field_by_name(segment)
1576 except (AttributeError, KeyError):
1577 raise TypeError('Subfield %r from path does not exist.' % (parameter,))
1579 self.__validate_simple_subfield(parameter, field, segment_list,
1580 _segment_index=_segment_index + 1)
1582 def __validate_path_parameters(self, field, path_parameters):
1583 """Verifies that all path parameters correspond to an existing subfield.
1585 Args:
1586 field: An instance of a subclass of messages.Field. Should be the root
1587 level property name in each path parameter in path_parameters. For
1588 example, if the field is called 'foo', then each path parameter should
1589 begin with 'foo.'.
1590 path_parameters: A list of Strings representing URI parameter variables.
1592 Raises:
1593 TypeError: If one of the path parameters does not start with field.name.
1595 for param in path_parameters:
1596 segment_list = param.split('.')
1597 if segment_list[0] != field.name:
1598 raise TypeError('Subfield %r can\'t come from field %r.'
1599 % (param, field.name))
1600 self.__validate_simple_subfield(field.name, field, segment_list[1:])
1602 def __parameter_default(self, final_subfield):
1603 """Returns default value of final subfield if it has one.
1605 If this subfield comes from a field list returned from __field_to_subfields,
1606 none of the fields in the subfield list can have a default except the final
1607 one since they all must be message fields.
1609 Args:
1610 final_subfield: A simple field from the end of a subfield list.
1612 Returns:
1613 The default value of the subfield, if any exists, with the exception of an
1614 enum field, which will have its value cast to a string.
1616 if final_subfield.default:
1617 if isinstance(final_subfield, messages.EnumField):
1618 return final_subfield.default.name
1619 else:
1620 return final_subfield.default
1622 def __parameter_enum(self, final_subfield):
1623 """Returns enum descriptor of final subfield if it is an enum.
1625 An enum descriptor is a dictionary with keys as the names from the enum and
1626 each value is a dictionary with a single key "backendValue" and value equal
1627 to the same enum name used to stored it in the descriptor.
1629 The key "description" can also be used next to "backendValue", but protorpc
1630 Enum classes have no way of supporting a description for each value.
1632 Args:
1633 final_subfield: A simple field from the end of a subfield list.
1635 Returns:
1636 The enum descriptor for the field, if it's an enum descriptor, else
1637 returns None.
1639 if isinstance(final_subfield, messages.EnumField):
1640 enum_descriptor = {}
1641 for enum_value in final_subfield.type.to_dict().keys():
1642 enum_descriptor[enum_value] = {'backendValue': enum_value}
1643 return enum_descriptor
1645 def __parameter_descriptor(self, subfield_list):
1646 """Creates descriptor for a parameter using the subfields that define it.
1648 Each parameter is defined by a list of fields, with all but the last being
1649 a message field and the final being a simple (non-message) field.
1651 Many of the fields in the descriptor are determined solely by the simple
1652 field at the end, though some (such as repeated and required) take the whole
1653 chain of fields into consideration.
1655 Args:
1656 subfield_list: List of fields describing the parameter.
1658 Returns:
1659 Dictionary containing a descriptor for the parameter described by the list
1660 of fields.
1662 descriptor = {}
1663 final_subfield = subfield_list[-1]
1666 if all(subfield.required for subfield in subfield_list):
1667 descriptor['required'] = True
1670 descriptor['type'] = self.__field_to_parameter_type(final_subfield)
1673 default = self.__parameter_default(final_subfield)
1674 if default is not None:
1675 descriptor['default'] = default
1678 if any(subfield.repeated for subfield in subfield_list):
1679 descriptor['repeated'] = True
1682 enum_descriptor = self.__parameter_enum(final_subfield)
1683 if enum_descriptor is not None:
1684 descriptor['enum'] = enum_descriptor
1686 return descriptor
1688 def __add_parameters_from_field(self, field, path_parameters,
1689 params, param_order):
1690 """Adds all parameters in a field to a method parameters descriptor.
1692 Simple fields will only have one parameter, but a message field 'x' that
1693 corresponds to a message class with fields 'y' and 'z' will result in
1694 parameters 'x.y' and 'x.z', for example. The mapping from field to
1695 parameters is mostly handled by __field_to_subfields.
1697 Args:
1698 field: Field from which parameters will be added to the method descriptor.
1699 path_parameters: A list of parameters matched from a path for this field.
1700 For example for the hypothetical 'x' from above if the path was
1701 '/a/{x.z}/b/{other}' then this list would contain only the element
1702 'x.z' since 'other' does not match to this field.
1703 params: Dictionary with parameter names as keys and parameter descriptors
1704 as values. This will be updated for each parameter in the field.
1705 param_order: List of required parameter names to give them an order in the
1706 descriptor. All required parameters in the field will be added to this
1707 list.
1709 for subfield_list in self.__field_to_subfields(field):
1710 descriptor = self.__parameter_descriptor(subfield_list)
1712 qualified_name = '.'.join(subfield.name for subfield in subfield_list)
1713 in_path = qualified_name in path_parameters
1714 if descriptor.get('required', in_path):
1715 descriptor['required'] = True
1716 param_order.append(qualified_name)
1718 params[qualified_name] = descriptor
1720 def __params_descriptor_without_container(self, message_type,
1721 request_kind, path):
1722 """Describe parameters of a method which does not use a ResourceContainer.
1724 Makes sure that the path parameters are included in the message definition
1725 and adds any required fields and URL query parameters.
1727 This method is to preserve backwards compatibility and will be removed in
1728 a future release.
1730 Args:
1731 message_type: messages.Message class, Message with parameters to describe.
1732 request_kind: The type of request being made.
1733 path: string, HTTP path to method.
1735 Returns:
1736 A tuple (dict, list of string): Descriptor of the parameters, Order of the
1737 parameters.
1739 params = {}
1740 param_order = []
1742 path_parameter_dict = self.__get_path_parameters(path)
1743 for field in sorted(message_type.all_fields(), key=lambda f: f.number):
1744 matched_path_parameters = path_parameter_dict.get(field.name, [])
1745 self.__validate_path_parameters(field, matched_path_parameters)
1746 if matched_path_parameters or request_kind == self.__NO_BODY:
1747 self.__add_parameters_from_field(field, matched_path_parameters,
1748 params, param_order)
1750 return params, param_order
1755 def __params_descriptor(self, message_type, request_kind, path, method_id):
1756 """Describe the parameters of a method.
1758 If the message_type is not a ResourceContainer, will fall back to
1759 __params_descriptor_without_container (which will eventually be deprecated).
1761 If the message type is a ResourceContainer, then all path/query parameters
1762 will come from the ResourceContainer. This method will also make sure all
1763 path parameters are covered by the message fields.
1765 Args:
1766 message_type: messages.Message or ResourceContainer class, Message with
1767 parameters to describe.
1768 request_kind: The type of request being made.
1769 path: string, HTTP path to method.
1770 method_id: string, Unique method identifier (e.g. 'myapi.items.method')
1772 Returns:
1773 A tuple (dict, list of string): Descriptor of the parameters, Order of the
1774 parameters.
1776 path_parameter_dict = self.__get_path_parameters(path)
1778 if not isinstance(message_type, ResourceContainer):
1779 if path_parameter_dict:
1780 logging.warning('Method %s specifies path parameters but you are not '
1781 'using a ResourceContainer. This will fail in future '
1782 'releases; please switch to using ResourceContainer as '
1783 'soon as possible.', method_id)
1784 return self.__params_descriptor_without_container(
1785 message_type, request_kind, path)
1788 message_type = message_type.parameters_message_class()
1790 params = {}
1791 param_order = []
1794 for field_name, matched_path_parameters in path_parameter_dict.iteritems():
1795 field = message_type.field_by_name(field_name)
1796 self.__validate_path_parameters(field, matched_path_parameters)
1799 for field in sorted(message_type.all_fields(), key=lambda f: f.number):
1800 matched_path_parameters = path_parameter_dict.get(field.name, [])
1801 self.__add_parameters_from_field(field, matched_path_parameters,
1802 params, param_order)
1804 return params, param_order
1806 def __request_message_descriptor(self, request_kind, message_type, method_id,
1807 path):
1808 """Describes the parameters and body of the request.
1810 Args:
1811 request_kind: The type of request being made.
1812 message_type: messages.Message or ResourceContainer class. The message to
1813 describe.
1814 method_id: string, Unique method identifier (e.g. 'myapi.items.method')
1815 path: string, HTTP path to method.
1817 Returns:
1818 Dictionary describing the request.
1820 Raises:
1821 ValueError: if the method path and request required fields do not match
1823 descriptor = {}
1825 params, param_order = self.__params_descriptor(message_type, request_kind,
1826 path, method_id)
1828 if isinstance(message_type, ResourceContainer):
1829 message_type = message_type.body_message_class()
1831 if (request_kind == self.__NO_BODY or
1832 message_type == message_types.VoidMessage()):
1833 descriptor['body'] = 'empty'
1834 else:
1835 descriptor['body'] = 'autoTemplate(backendRequest)'
1836 descriptor['bodyName'] = 'resource'
1837 self.__request_schema[method_id] = self.__parser.add_message(
1838 message_type.__class__)
1840 if params:
1841 descriptor['parameters'] = params
1843 if param_order:
1844 descriptor['parameterOrder'] = param_order
1846 return descriptor
1848 def __response_message_descriptor(self, message_type, method_id,
1849 cache_control):
1850 """Describes the response.
1852 Args:
1853 message_type: messages.Message class, The message to describe.
1854 method_id: string, Unique method identifier (e.g. 'myapi.items.method')
1855 cache_control: CacheControl, Cache settings for the API method.
1857 Returns:
1858 Dictionary describing the response.
1860 descriptor = {}
1862 self.__parser.add_message(message_type.__class__)
1863 if message_type == message_types.VoidMessage():
1864 descriptor['body'] = 'empty'
1865 else:
1866 descriptor['body'] = 'autoTemplate(backendResponse)'
1867 descriptor['bodyName'] = 'resource'
1868 self.__response_schema[method_id] = self.__parser.ref_for_message_type(
1869 message_type.__class__)
1871 if cache_control is not None:
1872 descriptor['cacheControl'] = {
1873 'type': cache_control.directive,
1874 'maxAge': cache_control.max_age_seconds,
1877 return descriptor
1879 def __method_descriptor(self, service, service_name, method_info,
1880 protorpc_method_name, protorpc_method_info):
1881 """Describes a method.
1883 Args:
1884 service: endpoints.Service, Implementation of the API as a service.
1885 service_name: string, Name of the service.
1886 method_info: _MethodInfo, Configuration for the method.
1887 protorpc_method_name: string, Name of the method as given in the
1888 ProtoRPC implementation.
1889 protorpc_method_info: protorpc.remote._RemoteMethodInfo, ProtoRPC
1890 description of the method.
1892 Returns:
1893 Dictionary describing the method.
1895 descriptor = {}
1897 request_message_type = ResourceContainer.get_request_message(
1898 protorpc_method_info.remote)
1899 request_kind = self.__get_request_kind(method_info)
1900 remote_method = protorpc_method_info.remote
1902 descriptor['path'] = method_info.get_path(service.api_info)
1903 descriptor['httpMethod'] = method_info.http_method
1904 descriptor['rosyMethod'] = '%s.%s' % (service_name, protorpc_method_name)
1905 descriptor['request'] = self.__request_message_descriptor(
1906 request_kind, request_message_type,
1907 method_info.method_id(service.api_info),
1908 descriptor['path'])
1909 descriptor['response'] = self.__response_message_descriptor(
1910 remote_method.response_type(), method_info.method_id(service.api_info),
1911 method_info.cache_control)
1916 scopes = (method_info.scopes
1917 if method_info.scopes is not None
1918 else service.api_info.scopes)
1919 if scopes:
1920 descriptor['scopes'] = scopes
1921 audiences = (method_info.audiences
1922 if method_info.audiences is not None
1923 else service.api_info.audiences)
1924 if audiences:
1925 descriptor['audiences'] = audiences
1926 allowed_client_ids = (method_info.allowed_client_ids
1927 if method_info.allowed_client_ids is not None
1928 else service.api_info.allowed_client_ids)
1929 if allowed_client_ids:
1930 descriptor['clientIds'] = allowed_client_ids
1932 if remote_method.method.__doc__:
1933 descriptor['description'] = remote_method.method.__doc__
1935 auth_level = (method_info.auth_level
1936 if method_info.auth_level is not None
1937 else service.api_info.auth_level)
1938 if auth_level is not None:
1939 descriptor['authLevel'] = AUTH_LEVEL.reverse_mapping[auth_level]
1941 return descriptor
1943 def __schema_descriptor(self, services):
1944 """Descriptor for the all the JSON Schema used.
1946 Args:
1947 services: List of protorpc.remote.Service instances implementing an
1948 api/version.
1950 Returns:
1951 Dictionary containing all the JSON Schema used in the service.
1953 methods_desc = {}
1955 for service in services:
1956 protorpc_methods = service.all_remote_methods()
1957 for protorpc_method_name in protorpc_methods.iterkeys():
1958 method_id = self.__id_from_name[protorpc_method_name]
1960 request_response = {}
1962 request_schema_id = self.__request_schema.get(method_id)
1963 if request_schema_id:
1964 request_response['request'] = {
1965 '$ref': request_schema_id
1968 response_schema_id = self.__response_schema.get(method_id)
1969 if response_schema_id:
1970 request_response['response'] = {
1971 '$ref': response_schema_id
1974 rosy_method = '%s.%s' % (service.__name__, protorpc_method_name)
1975 methods_desc[rosy_method] = request_response
1977 descriptor = {
1978 'methods': methods_desc,
1979 'schemas': self.__parser.schemas(),
1982 return descriptor
1984 def __get_merged_api_info(self, services):
1985 """Builds a description of an API.
1987 Args:
1988 services: List of protorpc.remote.Service instances implementing an
1989 api/version.
1991 Returns:
1992 The _ApiInfo object to use for the API that the given services implement.
1994 Raises:
1995 ApiConfigurationError: If there's something wrong with the API
1996 configuration, such as a multiclass API decorated with different API
1997 descriptors (see the docstring for api()).
1999 merged_api_info = services[0].api_info
2003 for service in services[1:]:
2004 if not merged_api_info.is_same_api(service.api_info):
2005 raise ApiConfigurationError(_MULTICLASS_MISMATCH_ERROR_TEMPLATE % (
2006 service.api_info.name, service.api_info.version))
2008 return merged_api_info
2010 def __auth_descriptor(self, api_info):
2011 if api_info.auth is None:
2012 return None
2014 auth_descriptor = {}
2015 if api_info.auth.allow_cookie_auth is not None:
2016 auth_descriptor['allowCookieAuth'] = api_info.auth.allow_cookie_auth
2017 if api_info.auth.blocked_regions:
2018 auth_descriptor['blockedRegions'] = api_info.auth.blocked_regions
2020 return auth_descriptor
2022 def __frontend_limit_descriptor(self, api_info):
2023 if api_info.frontend_limits is None:
2024 return None
2026 descriptor = {}
2027 for propname, descname in (('unregistered_user_qps', 'unregisteredUserQps'),
2028 ('unregistered_qps', 'unregisteredQps'),
2029 ('unregistered_daily', 'unregisteredDaily')):
2030 if getattr(api_info.frontend_limits, propname) is not None:
2031 descriptor[descname] = getattr(api_info.frontend_limits, propname)
2033 rules = self.__frontend_limit_rules_descriptor(api_info)
2034 if rules:
2035 descriptor['rules'] = rules
2037 return descriptor
2039 def __frontend_limit_rules_descriptor(self, api_info):
2040 if not api_info.frontend_limits.rules:
2041 return None
2043 rules = []
2044 for rule in api_info.frontend_limits.rules:
2045 descriptor = {}
2046 for propname, descname in (('match', 'match'),
2047 ('qps', 'qps'),
2048 ('user_qps', 'userQps'),
2049 ('daily', 'daily'),
2050 ('analytics_id', 'analyticsId')):
2051 if getattr(rule, propname) is not None:
2052 descriptor[descname] = getattr(rule, propname)
2053 if descriptor:
2054 rules.append(descriptor)
2056 return rules
2058 def __api_descriptor(self, services, hostname=None):
2059 """Builds a description of an API.
2061 Args:
2062 services: List of protorpc.remote.Service instances implementing an
2063 api/version.
2064 hostname: string, Hostname of the API, to override the value set on the
2065 current service. Defaults to None.
2067 Returns:
2068 A dictionary that can be deserialized into JSON and stored as an API
2069 description document.
2071 Raises:
2072 ApiConfigurationError: If there's something wrong with the API
2073 configuration, such as a multiclass API decorated with different API
2074 descriptors (see the docstring for api()), or a repeated method
2075 signature.
2077 merged_api_info = self.__get_merged_api_info(services)
2078 descriptor = self.get_descriptor_defaults(merged_api_info,
2079 hostname=hostname)
2080 description = merged_api_info.description
2081 if not description and len(services) == 1:
2082 description = services[0].__doc__
2083 if description:
2084 descriptor['description'] = description
2086 auth_descriptor = self.__auth_descriptor(merged_api_info)
2087 if auth_descriptor:
2088 descriptor['auth'] = auth_descriptor
2090 frontend_limit_descriptor = self.__frontend_limit_descriptor(
2091 merged_api_info)
2092 if frontend_limit_descriptor:
2093 descriptor['frontendLimits'] = frontend_limit_descriptor
2095 method_map = {}
2096 method_collision_tracker = {}
2097 rest_collision_tracker = {}
2099 for service in services:
2100 remote_methods = service.all_remote_methods()
2101 for protorpc_meth_name, protorpc_meth_info in remote_methods.iteritems():
2102 method_info = getattr(protorpc_meth_info, 'method_info', None)
2104 if method_info is None:
2105 continue
2106 method_id = method_info.method_id(service.api_info)
2107 self.__id_from_name[protorpc_meth_name] = method_id
2108 method_map[method_id] = self.__method_descriptor(
2109 service, service.__name__, method_info,
2110 protorpc_meth_name, protorpc_meth_info)
2113 if method_id in method_collision_tracker:
2114 raise ApiConfigurationError(
2115 'Method %s used multiple times, in classes %s and %s' %
2116 (method_id, method_collision_tracker[method_id],
2117 service.__name__))
2118 else:
2119 method_collision_tracker[method_id] = service.__name__
2122 rest_identifier = (method_info.http_method,
2123 method_info.get_path(service.api_info))
2124 if rest_identifier in rest_collision_tracker:
2125 raise ApiConfigurationError(
2126 '%s path "%s" used multiple times, in classes %s and %s' %
2127 (method_info.http_method, method_info.get_path(service.api_info),
2128 rest_collision_tracker[rest_identifier],
2129 service.__name__))
2130 else:
2131 rest_collision_tracker[rest_identifier] = service.__name__
2133 if method_map:
2134 descriptor['methods'] = method_map
2135 descriptor['descriptor'] = self.__schema_descriptor(services)
2137 return descriptor
2139 def get_descriptor_defaults(self, api_info, hostname=None):
2140 """Gets a default configuration for a service.
2142 Args:
2143 api_info: _ApiInfo object for this service.
2144 hostname: string, Hostname of the API, to override the value set on the
2145 current service. Defaults to None.
2147 Returns:
2148 A dictionary with the default configuration.
2150 hostname = hostname or api_info.hostname
2151 defaults = {
2152 'extends': 'thirdParty.api',
2153 'root': 'https://%s/_ah/api' % hostname,
2154 'name': api_info.name,
2155 'version': api_info.version,
2156 'defaultVersion': True,
2157 'abstract': False,
2158 'adapter': {
2159 'bns': 'https://%s/_ah/spi' % hostname,
2160 'type': 'lily',
2161 'deadline': 10.0
2164 if api_info.canonical_name:
2165 defaults['canonicalName'] = api_info.canonical_name
2166 if api_info.owner_domain:
2167 defaults['ownerDomain'] = api_info.owner_domain
2168 if api_info.owner_name:
2169 defaults['ownerName'] = api_info.owner_name
2170 if api_info.package_path:
2171 defaults['packagePath'] = api_info.package_path
2172 if api_info.title:
2173 defaults['title'] = api_info.title
2174 if api_info.documentation:
2175 defaults['documentation'] = api_info.documentation
2176 return defaults
2178 def pretty_print_config_to_json(self, services, hostname=None):
2179 """Description of a protorpc.remote.Service in API format.
2181 Args:
2182 services: Either a single protorpc.remote.Service or a list of them
2183 that implements an api/version.
2184 hostname: string, Hostname of the API, to override the value set on the
2185 current service. Defaults to None.
2187 Returns:
2188 string, The API descriptor document as JSON.
2190 if not isinstance(services, (tuple, list)):
2191 services = [services]
2195 _CheckListType(services, remote._ServiceClass, 'services', allow_none=False)
2197 descriptor = self.__api_descriptor(services, hostname=hostname)
2198 return json.dumps(descriptor, sort_keys=True, indent=2)