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
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):
34 api = ApiConfigGenerator().pretty_print_config_to_json(MyService)
50 import simplejson
as json
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
63 from google
.appengine
.api
import app_identity
66 from google
.appengine
.api
import app_identity
70 'API_EXPLORER_CLIENT_ID',
73 'ApiConfigurationError',
74 'ApiFrontEndLimitRule',
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.
99 docstring: Docstring for the generated enum class.
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
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).
152 field: A ProtoRPC message field (potentially to be copied).
155 TypeError: If the field is not an instance of messages.Field.
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
,))
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.
188 field: A ProtoRPC message field to be copied.
189 number: An integer for the field to override the number of the field.
193 TypeError: If the field is not an instance of messages.Field.
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.
211 field: A ProtoRPC message field to be compared.
212 other_field: A ProtoRPC message field to be compared.
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
:
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
231 body_message_class: A message class to represent a request body.
232 parameters_message_class: A placeholder message class for request
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.
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
)
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.
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.
272 Value of combined message class for this property.
274 if self
.__combined
_message
_class
is not None:
275 return self
.__combined
_message
_class
286 for field
in self
.body_message_class
.all_fields():
287 fields
[field
.name
] = _CopyField(field
, number
=field_number
)
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
,))
297 fields
[field
.name
] = _CopyField(field
, number
=field_number
)
300 self
.__combined
_message
_class
= type('CombinedContainer',
301 (messages
.Message
,), fields
)
302 return self
.__combined
_message
_class
305 def add_to_cache(cls
, remote_info
, container
):
306 """Adds a ResourceContainer to a cache tying it to a protorpc method.
309 remote_info: Instance of protorpc.remote._RemoteMethodInfo corresponding
311 container: An instance of ResourceContainer.
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
319 if not isinstance(container
, cls
):
320 raise TypeError('%r not an instance of %r, could not be added to cache.' %
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
327 def get_request_message(cls
, remote_info
):
328 """Gets request message or container from remote info.
331 remote_info: Instance of protorpc.remote._RemoteMethodInfo corresponding
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
]
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.
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.
354 TypeError: if setting is not of the allowed type.
357 The list of settings, for convenient use in assignment.
361 raise TypeError('%s is None, which is not allowed.' % name
)
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
))
372 def _CheckType(value
, check_type
, name
, allow_none
=True):
373 """Check that the type of an object is acceptable.
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.
382 TypeError: If value is not an acceptable type.
384 if value
is None and allow_none
:
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
):
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).
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.
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.
420 audiences: list of strings, Acceptable audiences for authentication.
422 scopes: list of strings, Acceptable scopes for authentication.
424 allowed_client_ids: list of strings, Acceptable client IDs for auth.
426 auth_level: enum from AUTH_LEVEL, Frontend authentication level.
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
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
):
449 return self
.__common
_info
is other
.__common
_info
453 """Name of the API."""
454 return self
.__common
_info
.name
458 """Version of the API."""
459 return self
.__common
_info
.version
462 def description(self
):
463 """Description of the API."""
464 return self
.__common
_info
.description
468 """Hostname for the API."""
469 return self
.__common
_info
.hostname
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
480 """List of scopes accepted for the API, overriding the defaults."""
481 if self
.__scopes
is not None:
483 return self
.__common
_info
.scopes
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
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
500 def canonical_name(self
):
501 """Canonical name for the API."""
502 return self
.__common
_info
.canonical_name
506 """Authentication configuration information for this API."""
507 return self
.__common
_info
.auth
510 def owner_domain(self
):
511 """Domain of the owner of this API."""
512 return self
.__common
_info
.owner_domain
515 def owner_name(self
):
516 """Name of the owner of this API."""
517 return self
.__common
_info
.owner_name
520 def package_path(self
):
521 """Package this API belongs to, '/' delimited. Used by client libs."""
522 return self
.__common
_info
.package_path
525 def frontend_limits(self
):
526 """Optional query limits for unregistered developers."""
527 return self
.__common
_info
.frontend_limits
531 """Human readable name of this API."""
532 return self
.__common
_info
.title
535 def documentation(self
):
536 """Link to the documentation for this version of the API."""
537 return self
.__common
_info
.documentation
540 def resource_name(self
):
541 """Resource name for the class this decorates."""
542 return self
.__resource
_name
546 """Base path prepended to any method paths in the class this decorates."""
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
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.
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
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
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
589 title: string, the human readable title of your API. It is exposed in the
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
)
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
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).
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.
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
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
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')
678 hostname
= app_identity
.get_default_version_hostname()
679 if audiences
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
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
697 self
.__owner
_domain
= owner_domain
698 self
.__owner
_name
= owner_name
699 self
.__package
_path
= package_path
700 self
.__frontend
_limits
= frontend_limits
702 self
.__documentation
= documentation
703 self
.__auth
_level
= auth_level
707 """Name of the API."""
712 """Version of the API."""
713 return self
.__version
716 def description(self
):
717 """Description of the API."""
718 return self
.__description
722 """Hostname for the API."""
723 return self
.__hostname
727 """List of audiences accepted by default for the API."""
728 return self
.__audiences
732 """List of scopes accepted by default for the API."""
736 def allowed_client_ids(self
):
737 """List of client IDs accepted by default for the API."""
738 return self
.__allowed
_client
_ids
741 def auth_level(self
):
742 """Enum from AUTH_LEVEL specifying default frontend auth level."""
743 return self
.__auth
_level
746 def canonical_name(self
):
747 """Canonical name for the API."""
748 return self
.__canonical
_name
752 """Authentication configuration for this API."""
756 def owner_domain(self
):
757 """Domain of the owner of this API."""
758 return self
.__owner
_domain
761 def owner_name(self
):
762 """Name of the owner of this API."""
763 return self
.__owner
_name
766 def package_path(self
):
767 """Package this API belongs to, '/' delimited. Used by client libs."""
768 return self
.__package
_path
771 def frontend_limits(self
):
772 """Optional query limits for unregistered developers."""
773 return self
.__frontend
_limits
777 """Human readable name of this API."""
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.
789 service_class: remote.Service class, ProtoRPC service class being wrapped.
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.
804 resource_name: string, Resource name for the class this decorates.
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.
810 scopes: list of strings, Acceptable scopes for authentication.
812 allowed_client_ids: list of strings, Acceptable client IDs for auth.
814 auth_level: enum from AUTH_LEVEL, Frontend authentication level.
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.
825 api_class: remote.Service class, ProtoRPC service class being wrapped.
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
)
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.
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
858 blocked_regions: list of Strings, a list of 2-letter ISO region codes
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
868 def allow_cookie_auth(self
):
869 """Whether cookie authentication is allowed for this API."""
870 return self
.__allow
_cookie
_auth
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,
883 """Constructor for ApiFrontEndLimitRule.
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
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')
901 self
.__user
_qps
= user_qps
903 self
.__analytics
_id
= analytics_id
907 """The matching rule that defines this traffic segment."""
912 """The aggregate QPS for this segment."""
917 """The per-end-user QPS for this segment."""
918 return self
.__user
_qps
922 """The aggregate daily maximum for this segment."""
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.
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
961 def unregistered_user_qps(self
):
962 """Per-end-user QPS limit."""
963 return self
.__unregistered
_user
_qps
966 def unregistered_qps(self
):
967 """Aggregate QPS upper-bound for all unregistered traffic."""
968 return self
.__unregistered
_qps
971 def unregistered_daily(self
):
972 """Aggregate daily upper-bound for all unregistered traffic."""
973 return self
.__unregistered
_daily
977 """Custom rules used to apply limits to unregistered traffic."""
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):
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
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
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
1037 title: string, the human readable title of your API. It is exposed in the
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.
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.
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.
1069 NO_CACHE
= 'no-cache'
1070 VALID_VALUES
= (PUBLIC
, PRIVATE
, NO_CACHE
)
1072 def __init__(self
, directive
=NO_CACHE
, max_age_seconds
=0):
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
1085 def directive(self
):
1086 """The cache setting for this method, PUBLIC, PRIVATE, or NO_CACHE."""
1087 return self
.__directive
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
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):
1110 name: string, Name of the method, prepended with <apiname>. to make it
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.
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:]
1144 """Method name as specified in decorator or derived."""
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.
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
1159 This method's request path (not including the http://.../_ah/api/ prefix).
1162 ApiConfigurationError: If the path isn't properly formatted.
1164 path
= self
.__path
or ''
1165 if path
and path
[0] == '/':
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)' %
1182 def http_method(self
):
1183 """HTTP method supported by the method (e.g. GET, POST)."""
1184 return self
.__http
_method
1187 def cache_control(self
):
1188 """Cache control setting for the API method."""
1189 return self
.__cache
_control
1193 """List of scopes for the API method."""
1194 return self
.__scopes
1197 def audiences(self
):
1198 """List of audiences for the API method."""
1199 return self
.__audiences
1202 def allowed_client_ids(self
):
1203 """List of allowed client IDs for the API method."""
1204 return self
.__allowed
_client
_ids
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
)
1220 return '%s%s.%s' % (self
.__safe
_name
(api_info
.name
), resource_part
,
1221 self
.__safe
_name
(self
.name
))
1225 def method(request_message
=message_types
.VoidMessage
,
1226 response_message
=message_types
.VoidMessage
,
1233 allowed_client_ids
=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.
1241 @api_config.method(RequestMessage, ResponseMessage,
1242 name='insert', http_method='PUT')
1243 def greeting_insert(request):
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.
1262 'apiserving_method_wrapper' function.
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.
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.
1283 TypeError: if setting is not of the allowed type.
1286 The setting, for convenient use in assignment.
1288 if (setting
is None and allow_none
or
1289 isinstance(setting
, allowed_type
)):
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.
1297 api_method: Original method being wrapped.
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.
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
,
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),
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.
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!' %
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.
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.
1397 method_info: _MethodInfo, method information.
1400 The kind of request.
1402 if method_info
.http_method
in ('GET', 'DELETE'):
1403 return self
.__NO
_BODY
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
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">],
1456 field: An instance of a subclass of messages.Field.
1459 A list of lists, where each sublist is a list of fields.
1462 if not isinstance(field
, messages
.MessageField
):
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
)
1478 def __field_to_parameter_type(self
, field
):
1479 """Converts the field variant type into a string describing the parameter.
1482 field: An instance of a subclass of messages.Field.
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.
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.
1524 path: String; a URI path, potentially with some parameters.
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
,
1539 """Verifies that a proposed subfield actually exists and is a simple field.
1541 Here, simple means it is not a MessageField (nested).
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
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.
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
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
))
1572 segment
= segment_list
[_segment_index
]
1573 parameter
+= '.' + segment
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.
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
1590 path_parameters: A list of Strings representing URI parameter variables.
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.
1610 final_subfield: A simple field from the end of a subfield list.
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
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.
1633 final_subfield: A simple field from the end of a subfield list.
1636 The enum descriptor for the field, if it's an enum descriptor, else
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.
1656 subfield_list: List of fields describing the parameter.
1659 Dictionary containing a descriptor for the parameter described by the list
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
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.
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
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
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.
1736 A tuple (dict, list of string): Descriptor of the parameters, Order of the
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.
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')
1773 A tuple (dict, list of string): Descriptor of the parameters, Order of the
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()
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
,
1808 """Describes the parameters and body of the request.
1811 request_kind: The type of request being made.
1812 message_type: messages.Message or ResourceContainer class. The message to
1814 method_id: string, Unique method identifier (e.g. 'myapi.items.method')
1815 path: string, HTTP path to method.
1818 Dictionary describing the request.
1821 ValueError: if the method path and request required fields do not match
1825 params
, param_order
= self
.__params
_descriptor
(message_type
, request_kind
,
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'
1835 descriptor
['body'] = 'autoTemplate(backendRequest)'
1836 descriptor
['bodyName'] = 'resource'
1837 self
.__request
_schema
[method_id
] = self
.__parser
.add_message(
1838 message_type
.__class
__)
1841 descriptor
['parameters'] = params
1844 descriptor
['parameterOrder'] = param_order
1848 def __response_message_descriptor(self
, message_type
, method_id
,
1850 """Describes the response.
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.
1858 Dictionary describing the response.
1862 self
.__parser
.add_message(message_type
.__class
__)
1863 if message_type
== message_types
.VoidMessage():
1864 descriptor
['body'] = 'empty'
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
,
1879 def __method_descriptor(self
, service
, service_name
, method_info
,
1880 protorpc_method_name
, protorpc_method_info
):
1881 """Describes a method.
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.
1893 Dictionary describing the method.
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
),
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
)
1920 descriptor
['scopes'] = scopes
1921 audiences
= (method_info
.audiences
1922 if method_info
.audiences
is not None
1923 else service
.api_info
.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
]
1943 def __schema_descriptor(self
, services
):
1944 """Descriptor for the all the JSON Schema used.
1947 services: List of protorpc.remote.Service instances implementing an
1951 Dictionary containing all the JSON Schema used in the service.
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
1978 'methods': methods_desc
,
1979 'schemas': self
.__parser
.schemas(),
1984 def __get_merged_api_info(self
, services
):
1985 """Builds a description of an API.
1988 services: List of protorpc.remote.Service instances implementing an
1992 The _ApiInfo object to use for the API that the given services implement.
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:
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:
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
)
2035 descriptor
['rules'] = rules
2039 def __frontend_limit_rules_descriptor(self
, api_info
):
2040 if not api_info
.frontend_limits
.rules
:
2044 for rule
in api_info
.frontend_limits
.rules
:
2046 for propname
, descname
in (('match', 'match'),
2048 ('user_qps', 'userQps'),
2050 ('analytics_id', 'analyticsId')):
2051 if getattr(rule
, propname
) is not None:
2052 descriptor
[descname
] = getattr(rule
, propname
)
2054 rules
.append(descriptor
)
2058 def __api_descriptor(self
, services
, hostname
=None):
2059 """Builds a description of an API.
2062 services: List of protorpc.remote.Service instances implementing an
2064 hostname: string, Hostname of the API, to override the value set on the
2065 current service. Defaults to None.
2068 A dictionary that can be deserialized into JSON and stored as an API
2069 description document.
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
2077 merged_api_info
= self
.__get
_merged
_api
_info
(services
)
2078 descriptor
= self
.get_descriptor_defaults(merged_api_info
,
2080 description
= merged_api_info
.description
2081 if not description
and len(services
) == 1:
2082 description
= services
[0].__doc
__
2084 descriptor
['description'] = description
2086 auth_descriptor
= self
.__auth
_descriptor
(merged_api_info
)
2088 descriptor
['auth'] = auth_descriptor
2090 frontend_limit_descriptor
= self
.__frontend
_limit
_descriptor
(
2092 if frontend_limit_descriptor
:
2093 descriptor
['frontendLimits'] = frontend_limit_descriptor
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:
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
],
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
],
2131 rest_collision_tracker
[rest_identifier
] = service
.__name
__
2134 descriptor
['methods'] = method_map
2135 descriptor
['descriptor'] = self
.__schema
_descriptor
(services
)
2139 def get_descriptor_defaults(self
, api_info
, hostname
=None):
2140 """Gets a default configuration for a service.
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.
2148 A dictionary with the default configuration.
2150 hostname
= hostname
or api_info
.hostname
2152 'extends': 'thirdParty.api',
2153 'root': 'https://%s/_ah/api' % hostname
,
2154 'name': api_info
.name
,
2155 'version': api_info
.version
,
2156 'defaultVersion': True,
2159 'bns': 'https://%s/_ah/spi' % hostname
,
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
2173 defaults
['title'] = api_info
.title
2174 if api_info
.documentation
:
2175 defaults
['documentation'] = api_info
.documentation
2178 def pretty_print_config_to_json(self
, services
, hostname
=None):
2179 """Description of a protorpc.remote.Service in API format.
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.
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)