blob: e24cd57298cd2318a45147a8680b5c9d26334770 [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001# Copyright 2016 Google Inc. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Library for generating an API configuration document for a ProtoRPC backend.
16
17The protorpc.remote.Service is inspected and a JSON document describing
18the API is returned.
19
20 class MyResponse(messages.Message):
21 bool_value = messages.BooleanField(1)
22 int32_value = messages.IntegerField(2)
23
24 class MyService(remote.Service):
25
26 @remote.method(message_types.VoidMessage, MyResponse)
27 def entries_get(self, request):
28 pass
29
30 api = ApiConfigGenerator().pretty_print_config_to_json(MyService)
31"""
32
33# pylint: disable=g-bad-name
34
35# pylint: disable=g-statement-before-imports,g-import-not-at-top
36from __future__ import absolute_import
37
38import json
39import logging
40import re
41import six
42
43from google.appengine.api import app_identity
44
45import attr
46from protorpc import util
47
48from . import api_exceptions
49from . import constants
50from . import message_parser
51from . import message_types
52from . import messages
53from . import remote
54from . import resource_container
55from . import types as endpoints_types
56# originally in this module
57from .types import Issuer, LimitDefinition, Namespace
58from . import users_id_token
59from . import util as endpoints_util
60
61_logger = logging.getLogger(__name__)
62package = 'google.appengine.endpoints'
63
64
65__all__ = [
66 'ApiAuth',
67 'ApiConfigGenerator',
68 'ApiFrontEndLimitRule',
69 'ApiFrontEndLimits',
70 'EMAIL_SCOPE',
71 'Issuer',
72 'LimitDefinition',
73 'Namespace',
74 'api',
75 'method',
76 'AUTH_LEVEL',
77 'package',
78]
79
80
81EMAIL_SCOPE = 'https://www.googleapis.com/auth/userinfo.email'
82_EMAIL_SCOPE_DESCRIPTION = 'View your email address'
83_EMAIL_SCOPE_OBJ = endpoints_types.OAuth2Scope(
84 scope=EMAIL_SCOPE, description=_EMAIL_SCOPE_DESCRIPTION)
85_PATH_VARIABLE_PATTERN = r'{([a-zA-Z_][a-zA-Z_.\d]*)}'
86
87_MULTICLASS_MISMATCH_ERROR_TEMPLATE = (
88 'Attempting to implement service %s, version %s, with multiple '
89 'classes that aren\'t compatible. See docstring for api() for '
90 'examples how to implement a multi-class API.')
91
92_INVALID_NAMESPACE_ERROR_TEMPLATE = (
93 'Invalid namespace configuration. If a namespace is set, make sure to set '
94 '%s. package_path is optional.')
95
96
97_VALID_PART_RE = re.compile('^{[^{}]+}$')
98_VALID_LAST_PART_RE = re.compile('^{[^{}]+}(:)?(?(1)[^{}]+)$')
99
100
101
102def _Enum(docstring, *names):
103 """Utility to generate enum classes used by annotations.
104
105 Args:
106 docstring: Docstring for the generated enum class.
107 *names: Enum names.
108
109 Returns:
110 A class that contains enum names as attributes.
111 """
112 enums = dict(zip(names, range(len(names))))
113 reverse = dict((value, key) for key, value in enums.items())
114 enums['reverse_mapping'] = reverse
115 enums['__doc__'] = docstring
116 return type('Enum', (object,), enums)
117
118_AUTH_LEVEL_DOCSTRING = """
119 Define the enums used by the auth_level annotation to specify frontend
120 authentication requirement.
121
122 Frontend authentication is handled by a Google API server prior to the
123 request reaching backends. An early return before hitting the backend can
124 happen if the request does not fulfil the requirement specified by the
125 auth_level.
126
127 Valid values of auth_level and their meanings are:
128
129 AUTH_LEVEL.REQUIRED: Valid authentication credentials are required. Backend
130 will be called only if authentication credentials are present and valid.
131
132 AUTH_LEVEL.OPTIONAL: Authentication is optional. If authentication credentials
133 are supplied they must be valid. Backend will be called if the request
134 contains valid authentication credentials or no authentication credentials.
135
136 AUTH_LEVEL.OPTIONAL_CONTINUE: Authentication is optional and will be attempted
137 if authentication credentials are supplied. Invalid authentication
138 credentials will be removed but the request can always reach backend.
139
140 AUTH_LEVEL.NONE: Frontend authentication will be skipped. If authentication is
141 desired, it will need to be performed by the backend.
142 """
143
144AUTH_LEVEL = _Enum(_AUTH_LEVEL_DOCSTRING, 'REQUIRED', 'OPTIONAL',
145 'OPTIONAL_CONTINUE', 'NONE')
146_AUTH_LEVEL_WARNING = ("Due to a design error, auth_level has never actually been functional. "
147 "It will likely be removed and replaced by a functioning alternative "
148 "in a future version of the framework. Please stop using auth_level now.")
149
150
151def _GetFieldAttributes(field):
152 """Decomposes field into the needed arguments to pass to the constructor.
153
154 This can be used to create copies of the field or to compare if two fields
155 are "equal" (since __eq__ is not implemented on messages.Field).
156
157 Args:
158 field: A ProtoRPC message field (potentially to be copied).
159
160 Raises:
161 TypeError: If the field is not an instance of messages.Field.
162
163 Returns:
164 A pair of relevant arguments to be passed to the constructor for the field
165 type. The first element is a list of positional arguments for the
166 constructor and the second is a dictionary of keyword arguments.
167 """
168 if not isinstance(field, messages.Field):
169 raise TypeError('Field %r to be copied not a ProtoRPC field.' % (field,))
170
171 positional_args = []
172 kwargs = {
173 'required': field.required,
174 'repeated': field.repeated,
175 'variant': field.variant,
176 'default': field._Field__default, # pylint: disable=protected-access
177 }
178
179 if isinstance(field, messages.MessageField):
180 # Message fields can't have a default
181 kwargs.pop('default')
182 if not isinstance(field, message_types.DateTimeField):
183 positional_args.insert(0, field.message_type)
184 elif isinstance(field, messages.EnumField):
185 positional_args.insert(0, field.type)
186
187 return positional_args, kwargs
188
189
190def _CheckType(value, check_type, name, allow_none=True):
191 """Check that the type of an object is acceptable.
192
193 Args:
194 value: The object whose type is to be checked.
195 check_type: The type that the object must be an instance of.
196 name: Name of the object, to be placed in any error messages.
197 allow_none: True if value can be None, false if not.
198
199 Raises:
200 TypeError: If value is not an acceptable type.
201 """
202 if value is None and allow_none:
203 return
204 if not isinstance(value, check_type):
205 raise TypeError('%s type doesn\'t match %s.' % (name, check_type))
206
207
208def _CheckEnum(value, check_type, name):
209 if value is None:
210 return
211 if value not in check_type.reverse_mapping:
212 raise TypeError('%s is not a valid value for %s' % (value, name))
213
214
215def _CheckNamespace(namespace):
216 _CheckType(namespace, Namespace, 'namespace')
217 if namespace:
218 if not namespace.owner_domain:
219 raise api_exceptions.InvalidNamespaceException(
220 _INVALID_NAMESPACE_ERROR_TEMPLATE % 'owner_domain')
221 if not namespace.owner_name:
222 raise api_exceptions.InvalidNamespaceException(
223 _INVALID_NAMESPACE_ERROR_TEMPLATE % 'owner_name')
224
225 _CheckType(namespace.owner_domain, six.string_types, 'namespace.owner_domain')
226 _CheckType(namespace.owner_name, six.string_types, 'namespace.owner_name')
227 _CheckType(namespace.package_path, six.string_types, 'namespace.package_path')
228
229
230def _CheckAudiences(audiences):
231 # Audiences can either be a list of audiences using the google_id_token
232 # or a dict mapping auth issuer name to the list of audiences.
233 if audiences is None or isinstance(audiences, dict):
234 return
235 else:
236 endpoints_util.check_list_type(audiences, six.string_types, 'audiences')
237
238
239def _CheckLimitDefinitions(limit_definitions):
240 _CheckType(limit_definitions, list, 'limit_definitions')
241 if limit_definitions:
242 for ld in limit_definitions:
243 if not ld.metric_name:
244 raise api_exceptions.InvalidLimitDefinitionException(
245 "Metric name must be set in all limit definitions.")
246 if not ld.display_name:
247 raise api_exceptions.InvalidLimitDefinitionException(
248 "Display name must be set in all limit definitions.")
249
250 _CheckType(ld.metric_name, six.string_types, 'limit_definition.metric_name')
251 _CheckType(ld.display_name, six.string_types, 'limit_definition.display_name')
252 _CheckType(ld.default_limit, int, 'limit_definition.default_limit')
253
254
255# pylint: disable=g-bad-name
256class _ApiInfo(object):
257 """Configurable attributes of an API.
258
259 A structured data object used to store API information associated with each
260 remote.Service-derived class that implements an API. This stores properties
261 that could be different for each class (such as the path or
262 collection/resource name), as well as properties common to all classes in
263 the API (such as API name and version).
264 """
265
266 @util.positional(2)
267 def __init__(self, common_info, resource_name=None, path=None, audiences=None,
268 scopes=None, allowed_client_ids=None, auth_level=None,
269 api_key_required=None):
270 """Constructor for _ApiInfo.
271
272 Args:
273 common_info: _ApiDecorator.__ApiCommonInfo, Information that's common for
274 all classes that implement an API.
275 resource_name: string, The collection that the annotated class will
276 implement in the API. (Default: None)
277 path: string, Base request path for all methods in this API.
278 (Default: None)
279 audiences: list of strings, Acceptable audiences for authentication.
280 (Default: None)
281 scopes: list of strings, Acceptable scopes for authentication.
282 (Default: None)
283 allowed_client_ids: list of strings, Acceptable client IDs for auth.
284 (Default: None)
285 auth_level: enum from AUTH_LEVEL, Frontend authentication level.
286 (Default: None)
287 api_key_required: bool, whether a key is required to call this API.
288 """
289 _CheckType(resource_name, six.string_types, 'resource_name')
290 _CheckType(path, six.string_types, 'path')
291 endpoints_util.check_list_type(audiences, six.string_types, 'audiences')
292 endpoints_util.check_list_type(scopes, six.string_types, 'scopes')
293 endpoints_util.check_list_type(allowed_client_ids, six.string_types,
294 'allowed_client_ids')
295 _CheckEnum(auth_level, AUTH_LEVEL, 'auth_level')
296 _CheckType(api_key_required, bool, 'api_key_required')
297
298 self.__common_info = common_info
299 self.__resource_name = resource_name
300 self.__path = path
301 self.__audiences = audiences
302 self.__scopes = endpoints_types.OAuth2Scope.convert_list(scopes)
303 self.__allowed_client_ids = allowed_client_ids
304 self.__auth_level = auth_level
305 self.__api_key_required = api_key_required
306
307 def is_same_api(self, other):
308 """Check if this implements the same API as another _ApiInfo instance."""
309 if not isinstance(other, _ApiInfo):
310 return False
311 # pylint: disable=protected-access
312 return self.__common_info is other.__common_info
313
314 @property
315 def name(self):
316 """Name of the API."""
317 return self.__common_info.name
318
319 @property
320 def api_version(self):
321 """Version of the API."""
322 return self.__common_info.api_version
323
324 @property
325 def path_version(self):
326 """Version of the API for putting in the path."""
327 return self.__common_info.path_version
328
329 @property
330 def description(self):
331 """Description of the API."""
332 return self.__common_info.description
333
334 @property
335 def hostname(self):
336 """Hostname for the API."""
337 return self.__common_info.hostname
338
339 @property
340 def audiences(self):
341 """List of audiences accepted for the API, overriding the defaults."""
342 if self.__audiences is not None:
343 return self.__audiences
344 return self.__common_info.audiences
345
346 @property
347 def scope_objs(self):
348 """List of scopes (as OAuth2Scopes) accepted for the API, overriding the defaults."""
349 if self.__scopes is not None:
350 return self.__scopes
351 return self.__common_info.scope_objs
352
353 @property
354 def scopes(self):
355 """List of scopes (as strings) accepted for the API, overriding the defaults."""
356 if self.scope_objs is not None:
357 return [_s.scope for _s in self.scope_objs]
358
359 @property
360 def allowed_client_ids(self):
361 """List of client IDs accepted for the API, overriding the defaults."""
362 if self.__allowed_client_ids is not None:
363 return self.__allowed_client_ids
364 return self.__common_info.allowed_client_ids
365
366 @property
367 def issuers(self):
368 """Dict mapping auth issuer names to auth issuers for the API."""
369 return self.__common_info.issuers
370
371 @property
372 def namespace(self):
373 """Namespace for the API."""
374 return self.__common_info.namespace
375
376 @property
377 def auth_level(self):
378 """Enum from AUTH_LEVEL specifying the frontend authentication level."""
379 if self.__auth_level is not None:
380 return self.__auth_level
381 return self.__common_info.auth_level
382
383 @property
384 def api_key_required(self):
385 """bool specifying whether a key is required to call into this API."""
386 if self.__api_key_required is not None:
387 return self.__api_key_required
388 return self.__common_info.api_key_required
389
390 @property
391 def canonical_name(self):
392 """Canonical name for the API."""
393 return self.__common_info.canonical_name
394
395 @property
396 def auth(self):
397 """Authentication configuration information for this API."""
398 return self.__common_info.auth
399
400 @property
401 def owner_domain(self):
402 """Domain of the owner of this API."""
403 return self.__common_info.owner_domain
404
405 @property
406 def owner_name(self):
407 """Name of the owner of this API."""
408 return self.__common_info.owner_name
409
410 @property
411 def package_path(self):
412 """Package this API belongs to, '/' delimited. Used by client libs."""
413 return self.__common_info.package_path
414
415 @property
416 def frontend_limits(self):
417 """Optional query limits for unregistered developers."""
418 return self.__common_info.frontend_limits
419
420 @property
421 def title(self):
422 """Human readable name of this API."""
423 return self.__common_info.title
424
425 @property
426 def documentation(self):
427 """Link to the documentation for this version of the API."""
428 return self.__common_info.documentation
429
430 @property
431 def resource_name(self):
432 """Resource name for the class this decorates."""
433 return self.__resource_name
434
435 @property
436 def path(self):
437 """Base path prepended to any method paths in the class this decorates."""
438 return self.__path
439
440 @property
441 def base_path(self):
442 """Base path for the entire API prepended before the path property."""
443 return self.__common_info.base_path
444
445 @property
446 def limit_definitions(self):
447 """Rate limiting metric definitions for this API."""
448 return self.__common_info.limit_definitions
449
450 @property
451 def use_request_uri(self):
452 """Match request paths based on the REQUEST_URI instead of PATH_INFO."""
453 return self.__common_info.use_request_uri
454
455
456class _ApiDecorator(object):
457 """Decorator for single- or multi-class APIs.
458
459 An instance of this class can be used directly as a decorator for a
460 single-class API. Or call the api_class() method to decorate a multi-class
461 API.
462 """
463
464 @util.positional(3)
465 def __init__(self, name, version, description=None, hostname=None,
466 audiences=None, scopes=None, allowed_client_ids=None,
467 canonical_name=None, auth=None, owner_domain=None,
468 owner_name=None, package_path=None, frontend_limits=None,
469 title=None, documentation=None, auth_level=None, issuers=None,
470 namespace=None, api_key_required=None, base_path=None,
471 limit_definitions=None, use_request_uri=None):
472 """Constructor for _ApiDecorator.
473
474 Args:
475 name: string, Name of the API.
476 version: string, Version of the API.
477 description: string, Short description of the API (Default: None)
478 hostname: string, Hostname of the API (Default: app engine default host)
479 audiences: list of strings, Acceptable audiences for authentication.
480 scopes: list of strings, Acceptable scopes for authentication.
481 allowed_client_ids: list of strings, Acceptable client IDs for auth.
482 canonical_name: string, the canonical name for the API, a more human
483 readable version of the name.
484 auth: ApiAuth instance, the authentication configuration information
485 for this API.
486 owner_domain: string, the domain of the person or company that owns
487 this API. Along with owner_name, this provides hints to properly
488 name client libraries for this API.
489 owner_name: string, the name of the owner of this API. Along with
490 owner_domain, this provides hints to properly name client libraries
491 for this API.
492 package_path: string, the "package" this API belongs to. This '/'
493 delimited value specifies logical groupings of APIs. This is used by
494 client libraries of this API.
495 frontend_limits: ApiFrontEndLimits, optional query limits for unregistered
496 developers.
497 title: string, the human readable title of your API. It is exposed in the
498 discovery service.
499 documentation: string, a URL where users can find documentation about this
500 version of the API. This will be surfaced in the API Explorer and GPE
501 plugin to allow users to learn about your service.
502 auth_level: enum from AUTH_LEVEL, Frontend authentication level.
503 issuers: dict, mapping auth issuer names to endpoints.Issuer objects.
504 namespace: endpoints.Namespace, the namespace for the API.
505 api_key_required: bool, whether a key is required to call this API.
506 base_path: string, the base path for all endpoints in this API.
507 limit_definitions: list of LimitDefinition tuples used in this API.
508 use_request_uri: if true, match requests against REQUEST_URI instead of PATH_INFO
509 """
510 self.__common_info = self.__ApiCommonInfo(
511 name, version, description=description, hostname=hostname,
512 audiences=audiences, scopes=scopes,
513 allowed_client_ids=allowed_client_ids,
514 canonical_name=canonical_name, auth=auth, owner_domain=owner_domain,
515 owner_name=owner_name, package_path=package_path,
516 frontend_limits=frontend_limits, title=title,
517 documentation=documentation, auth_level=auth_level, issuers=issuers,
518 namespace=namespace, api_key_required=api_key_required,
519 base_path=base_path, limit_definitions=limit_definitions,
520 use_request_uri=use_request_uri)
521 self.__classes = []
522
523 class __ApiCommonInfo(object):
524 """API information that's common among all classes that implement an API.
525
526 When a remote.Service-derived class implements part of an API, there is
527 some common information that remains constant across all such classes
528 that implement the same API. This includes things like name, version,
529 hostname, and so on. __ApiComminInfo stores that common information, and
530 a single __ApiCommonInfo instance is shared among all classes that
531 implement the same API, guaranteeing that they share the same common
532 information.
533
534 Some of these values can be overridden (such as audiences and scopes),
535 while some can't and remain the same for all classes that implement
536 the API (such as name and version).
537 """
538
539 @util.positional(3)
540 def __init__(self, name, version, description=None, hostname=None,
541 audiences=None, scopes=None, allowed_client_ids=None,
542 canonical_name=None, auth=None, owner_domain=None,
543 owner_name=None, package_path=None, frontend_limits=None,
544 title=None, documentation=None, auth_level=None, issuers=None,
545 namespace=None, api_key_required=None, base_path=None,
546 limit_definitions=None, use_request_uri=None):
547 """Constructor for _ApiCommonInfo.
548
549 Args:
550 name: string, Name of the API.
551 version: string, Version of the API.
552 description: string, Short description of the API (Default: None)
553 hostname: string, Hostname of the API (Default: app engine default host)
554 audiences: list of strings, Acceptable audiences for authentication.
555 scopes: list of strings, Acceptable scopes for authentication.
556 allowed_client_ids: list of strings, Acceptable client IDs for auth.
557 canonical_name: string, the canonical name for the API, a more human
558 readable version of the name.
559 auth: ApiAuth instance, the authentication configuration information
560 for this API.
561 owner_domain: string, the domain of the person or company that owns
562 this API. Along with owner_name, this provides hints to properly
563 name client libraries for this API.
564 owner_name: string, the name of the owner of this API. Along with
565 owner_domain, this provides hints to properly name client libraries
566 for this API.
567 package_path: string, the "package" this API belongs to. This '/'
568 delimited value specifies logical groupings of APIs. This is used by
569 client libraries of this API.
570 frontend_limits: ApiFrontEndLimits, optional query limits for
571 unregistered developers.
572 title: string, the human readable title of your API. It is exposed in
573 the discovery service.
574 documentation: string, a URL where users can find documentation about
575 this version of the API. This will be surfaced in the API Explorer and
576 GPE plugin to allow users to learn about your service.
577 auth_level: enum from AUTH_LEVEL, Frontend authentication level.
578 issuers: dict, mapping auth issuer names to endpoints.Issuer objects.
579 namespace: endpoints.Namespace, the namespace for the API.
580 api_key_required: bool, whether a key is required to call into this API.
581 base_path: string, the base path for all endpoints in this API.
582 limit_definitions: list of LimitDefinition tuples used in this API.
583 use_request_uri: if true, match requests against REQUEST_URI instead of PATH_INFO
584 """
585 _CheckType(name, six.string_types, 'name', allow_none=False)
586 _CheckType(version, six.string_types, 'version', allow_none=False)
587 _CheckType(description, six.string_types, 'description')
588 _CheckType(hostname, six.string_types, 'hostname')
589 endpoints_util.check_list_type(scopes, (six.string_types, endpoints_types.OAuth2Scope), 'scopes')
590 endpoints_util.check_list_type(allowed_client_ids, six.string_types,
591 'allowed_client_ids')
592 _CheckType(canonical_name, six.string_types, 'canonical_name')
593 _CheckType(auth, ApiAuth, 'auth')
594 _CheckType(owner_domain, six.string_types, 'owner_domain')
595 _CheckType(owner_name, six.string_types, 'owner_name')
596 _CheckType(package_path, six.string_types, 'package_path')
597 _CheckType(frontend_limits, ApiFrontEndLimits, 'frontend_limits')
598 _CheckType(title, six.string_types, 'title')
599 _CheckType(documentation, six.string_types, 'documentation')
600 _CheckEnum(auth_level, AUTH_LEVEL, 'auth_level')
601 _CheckType(api_key_required, bool, 'api_key_required')
602 _CheckType(base_path, six.string_types, 'base_path')
603
604 _CheckType(issuers, dict, 'issuers')
605 if issuers:
606 for issuer_name, issuer_value in issuers.items():
607 _CheckType(issuer_name, six.string_types, 'issuer %s' % issuer_name)
608 _CheckType(issuer_value, Issuer, 'issuer value for %s' % issuer_name)
609
610 _CheckNamespace(namespace)
611
612 _CheckAudiences(audiences)
613
614 _CheckLimitDefinitions(limit_definitions)
615 _CheckType(use_request_uri, bool, 'use_request_uri')
616
617 if hostname is None:
618 hostname = app_identity.get_default_version_hostname()
619 if scopes is None:
620 scopes = [_EMAIL_SCOPE_OBJ]
621 else:
622 scopes = endpoints_types.OAuth2Scope.convert_list(scopes)
623 if allowed_client_ids is None:
624 allowed_client_ids = [constants.API_EXPLORER_CLIENT_ID]
625 if auth_level is None:
626 auth_level = AUTH_LEVEL.NONE
627 if api_key_required is None:
628 api_key_required = False
629 if base_path is None:
630 base_path = '/_ah/api/'
631 if use_request_uri is None:
632 use_request_uri = False
633
634 self.__name = name
635 self.__api_version = version
636 self.__path_version = version
637 self.__description = description
638 self.__hostname = hostname
639 self.__audiences = audiences
640 self.__scopes = scopes
641 self.__allowed_client_ids = allowed_client_ids
642 self.__canonical_name = canonical_name
643 self.__auth = auth
644 self.__owner_domain = owner_domain
645 self.__owner_name = owner_name
646 self.__package_path = package_path
647 self.__frontend_limits = frontend_limits
648 self.__title = title
649 self.__documentation = documentation
650 self.__auth_level = auth_level
651 self.__issuers = issuers
652 self.__namespace = namespace
653 self.__api_key_required = api_key_required
654 self.__base_path = base_path
655 self.__limit_definitions = limit_definitions
656 self.__use_request_uri = use_request_uri
657
658 @property
659 def name(self):
660 """Name of the API."""
661 return self.__name
662
663 @property
664 def api_version(self):
665 """Version of the API."""
666 return self.__api_version
667
668 @property
669 def path_version(self):
670 """Version of the API for putting in the path."""
671 return self.__path_version
672
673 @property
674 def description(self):
675 """Description of the API."""
676 return self.__description
677
678 @property
679 def hostname(self):
680 """Hostname for the API."""
681 return self.__hostname
682
683 @property
684 def audiences(self):
685 """List of audiences accepted by default for the API."""
686 return self.__audiences
687
688 @property
689 def scope_objs(self):
690 """List of scopes (as OAuth2Scopes) accepted by default for the API."""
691 return self.__scopes
692
693 @property
694 def scopes(self):
695 """List of scopes (as strings) accepted by default for the API."""
696 if self.scope_objs is not None:
697 return [_s.scope for _s in self.scope_objs]
698
699 @property
700 def allowed_client_ids(self):
701 """List of client IDs accepted by default for the API."""
702 return self.__allowed_client_ids
703
704 @property
705 def issuers(self):
706 """List of auth issuers for the API."""
707 return self.__issuers
708
709 @property
710 def namespace(self):
711 """Namespace of the API."""
712 return self.__namespace
713
714 @property
715 def auth_level(self):
716 """Enum from AUTH_LEVEL specifying default frontend auth level."""
717 return self.__auth_level
718
719 @property
720 def canonical_name(self):
721 """Canonical name for the API."""
722 return self.__canonical_name
723
724 @property
725 def auth(self):
726 """Authentication configuration for this API."""
727 return self.__auth
728
729 @property
730 def api_key_required(self):
731 """Whether a key is required to call into this API."""
732 return self.__api_key_required
733
734 @property
735 def owner_domain(self):
736 """Domain of the owner of this API."""
737 return self.__owner_domain
738
739 @property
740 def owner_name(self):
741 """Name of the owner of this API."""
742 return self.__owner_name
743
744 @property
745 def package_path(self):
746 """Package this API belongs to, '/' delimited. Used by client libs."""
747 return self.__package_path
748
749 @property
750 def frontend_limits(self):
751 """Optional query limits for unregistered developers."""
752 return self.__frontend_limits
753
754 @property
755 def title(self):
756 """Human readable name of this API."""
757 return self.__title
758
759 @property
760 def documentation(self):
761 """Link to the documentation for this version of the API."""
762 return self.__documentation
763
764 @property
765 def base_path(self):
766 """The base path for all endpoints in this API."""
767 return self.__base_path
768
769 @property
770 def limit_definitions(self):
771 """Rate limiting metric definitions for this API."""
772 return self.__limit_definitions
773
774 @property
775 def use_request_uri(self):
776 """Match request paths based on the REQUEST_URI instead of PATH_INFO."""
777 return self.__use_request_uri
778
779 def __call__(self, service_class):
780 """Decorator for ProtoRPC class that configures Google's API server.
781
782 Args:
783 service_class: remote.Service class, ProtoRPC service class being wrapped.
784
785 Returns:
786 Same class with API attributes assigned in api_info.
787 """
788 return self.api_class()(service_class)
789
790 def api_class(self, resource_name=None, path=None, audiences=None,
791 scopes=None, allowed_client_ids=None, auth_level=None,
792 api_key_required=None):
793 """Get a decorator for a class that implements an API.
794
795 This can be used for single-class or multi-class implementations. It's
796 used implicitly in simple single-class APIs that only use @api directly.
797
798 Args:
799 resource_name: string, Resource name for the class this decorates.
800 (Default: None)
801 path: string, Base path prepended to any method paths in the class this
802 decorates. (Default: None)
803 audiences: list of strings, Acceptable audiences for authentication.
804 (Default: None)
805 scopes: list of strings, Acceptable scopes for authentication.
806 (Default: None)
807 allowed_client_ids: list of strings, Acceptable client IDs for auth.
808 (Default: None)
809 auth_level: enum from AUTH_LEVEL, Frontend authentication level.
810 (Default: None)
811 api_key_required: bool, Whether a key is required to call into this API.
812 (Default: None)
813
814 Returns:
815 A decorator function to decorate a class that implements an API.
816 """
817 if auth_level is not None:
818 _logger.warn(_AUTH_LEVEL_WARNING)
819
820 def apiserving_api_decorator(api_class):
821 """Decorator for ProtoRPC class that configures Google's API server.
822
823 Args:
824 api_class: remote.Service class, ProtoRPC service class being wrapped.
825
826 Returns:
827 Same class with API attributes assigned in api_info.
828 """
829 self.__classes.append(api_class)
830 api_class.api_info = _ApiInfo(
831 self.__common_info, resource_name=resource_name,
832 path=path, audiences=audiences, scopes=scopes,
833 allowed_client_ids=allowed_client_ids, auth_level=auth_level,
834 api_key_required=api_key_required)
835 return api_class
836
837 return apiserving_api_decorator
838
839 def get_api_classes(self):
840 """Get the list of remote.Service classes that implement this API."""
841 return self.__classes
842
843
844class ApiAuth(object):
845 """Optional authorization configuration information for an API."""
846
847 def __init__(self, allow_cookie_auth=None, blocked_regions=None):
848 """Constructor for ApiAuth, authentication information for an API.
849
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.
860 """
861 _CheckType(allow_cookie_auth, bool, 'allow_cookie_auth')
862 endpoints_util.check_list_type(blocked_regions, six.string_types,
863 'blocked_regions')
864
865 self.__allow_cookie_auth = allow_cookie_auth
866 self.__blocked_regions = blocked_regions
867
868 @property
869 def allow_cookie_auth(self):
870 """Whether cookie authentication is allowed for this API."""
871 return self.__allow_cookie_auth
872
873 @property
874 def blocked_regions(self):
875 """List of 2-letter ISO region codes to block."""
876 return self.__blocked_regions
877
878
879class ApiFrontEndLimitRule(object):
880 """Custom rule to limit unregistered traffic."""
881
882 def __init__(self, match=None, qps=None, user_qps=None, daily=None,
883 analytics_id=None):
884 """Constructor for ApiFrontEndLimitRule.
885
886 Args:
887 match: string, the matching rule that defines this traffic segment.
888 qps: int, the aggregate QPS for this segment.
889 user_qps: int, the per-end-user QPS for this segment.
890 daily: int, the aggregate daily maximum for this segment.
891 analytics_id: string, the project ID under which traffic for this segment
892 will be logged.
893 """
894 _CheckType(match, six.string_types, 'match')
895 _CheckType(qps, int, 'qps')
896 _CheckType(user_qps, int, 'user_qps')
897 _CheckType(daily, int, 'daily')
898 _CheckType(analytics_id, six.string_types, 'analytics_id')
899
900 self.__match = match
901 self.__qps = qps
902 self.__user_qps = user_qps
903 self.__daily = daily
904 self.__analytics_id = analytics_id
905
906 @property
907 def match(self):
908 """The matching rule that defines this traffic segment."""
909 return self.__match
910
911 @property
912 def qps(self):
913 """The aggregate QPS for this segment."""
914 return self.__qps
915
916 @property
917 def user_qps(self):
918 """The per-end-user QPS for this segment."""
919 return self.__user_qps
920
921 @property
922 def daily(self):
923 """The aggregate daily maximum for this segment."""
924 return self.__daily
925
926 @property
927 def analytics_id(self):
928 """Project ID under which traffic for this segment will be logged."""
929 return self.__analytics_id
930
931
932class ApiFrontEndLimits(object):
933 """Optional front end limit information for an API."""
934
935 def __init__(self, unregistered_user_qps=None, unregistered_qps=None,
936 unregistered_daily=None, rules=None):
937 """Constructor for ApiFrontEndLimits, front end limit info for an API.
938
939 Args:
940 unregistered_user_qps: int, the per-end-user QPS. Users are identified
941 by their IP address. A value of 0 will block unregistered requests.
942 unregistered_qps: int, an aggregate QPS upper-bound for all unregistered
943 traffic. A value of 0 currently means unlimited, though it might change
944 in the future. To block unregistered requests, use unregistered_user_qps
945 or unregistered_daily instead.
946 unregistered_daily: int, an aggregate daily upper-bound for all
947 unregistered traffic. A value of 0 will block unregistered requests.
948 rules: A list or tuple of ApiFrontEndLimitRule instances: custom rules
949 used to apply limits to unregistered traffic.
950 """
951 _CheckType(unregistered_user_qps, int, 'unregistered_user_qps')
952 _CheckType(unregistered_qps, int, 'unregistered_qps')
953 _CheckType(unregistered_daily, int, 'unregistered_daily')
954 endpoints_util.check_list_type(rules, ApiFrontEndLimitRule, 'rules')
955
956 self.__unregistered_user_qps = unregistered_user_qps
957 self.__unregistered_qps = unregistered_qps
958 self.__unregistered_daily = unregistered_daily
959 self.__rules = rules
960
961 @property
962 def unregistered_user_qps(self):
963 """Per-end-user QPS limit."""
964 return self.__unregistered_user_qps
965
966 @property
967 def unregistered_qps(self):
968 """Aggregate QPS upper-bound for all unregistered traffic."""
969 return self.__unregistered_qps
970
971 @property
972 def unregistered_daily(self):
973 """Aggregate daily upper-bound for all unregistered traffic."""
974 return self.__unregistered_daily
975
976 @property
977 def rules(self):
978 """Custom rules used to apply limits to unregistered traffic."""
979 return self.__rules
980
981
982@util.positional(2)
983def api(name, version, description=None, hostname=None, audiences=None,
984 scopes=None, allowed_client_ids=None, canonical_name=None,
985 auth=None, owner_domain=None, owner_name=None, package_path=None,
986 frontend_limits=None, title=None, documentation=None, auth_level=None,
987 issuers=None, namespace=None, api_key_required=None, base_path=None,
988 limit_definitions=None, use_request_uri=None):
989 """Decorate a ProtoRPC Service class for use by the framework above.
990
991 This decorator can be used to specify an API name, version, description, and
992 hostname for your API.
993
994 Sample usage (python 2.7):
995 @endpoints.api(name='guestbook', version='v0.2',
996 description='Guestbook API')
997 class PostService(remote.Service):
998 ...
999
1000 Sample usage (python 2.5):
1001 class PostService(remote.Service):
1002 ...
1003 endpoints.api(name='guestbook', version='v0.2',
1004 description='Guestbook API')(PostService)
1005
1006 Sample usage if multiple classes implement one API:
1007 api_root = endpoints.api(name='library', version='v1.0')
1008
1009 @api_root.api_class(resource_name='shelves')
1010 class Shelves(remote.Service):
1011 ...
1012
1013 @api_root.api_class(resource_name='books', path='books')
1014 class Books(remote.Service):
1015 ...
1016
1017 Args:
1018 name: string, Name of the API.
1019 version: string, Version of the API.
1020 description: string, Short description of the API (Default: None)
1021 hostname: string, Hostname of the API (Default: app engine default host)
1022 audiences: list of strings, Acceptable audiences for authentication.
1023 scopes: list of strings, Acceptable scopes for authentication.
1024 allowed_client_ids: list of strings, Acceptable client IDs for auth.
1025 canonical_name: string, the canonical name for the API, a more human
1026 readable version of the name.
1027 auth: ApiAuth instance, the authentication configuration information
1028 for this API.
1029 owner_domain: string, the domain of the person or company that owns
1030 this API. Along with owner_name, this provides hints to properly
1031 name client libraries for this API.
1032 owner_name: string, the name of the owner of this API. Along with
1033 owner_domain, this provides hints to properly name client libraries
1034 for this API.
1035 package_path: string, the "package" this API belongs to. This '/'
1036 delimited value specifies logical groupings of APIs. This is used by
1037 client libraries of this API.
1038 frontend_limits: ApiFrontEndLimits, optional query limits for unregistered
1039 developers.
1040 title: string, the human readable title of your API. It is exposed in the
1041 discovery service.
1042 documentation: string, a URL where users can find documentation about this
1043 version of the API. This will be surfaced in the API Explorer and GPE
1044 plugin to allow users to learn about your service.
1045 auth_level: enum from AUTH_LEVEL, frontend authentication level.
1046 issuers: dict, mapping auth issuer names to endpoints.Issuer objects.
1047 namespace: endpoints.Namespace, the namespace for the API.
1048 api_key_required: bool, whether a key is required to call into this API.
1049 base_path: string, the base path for all endpoints in this API.
1050 limit_definitions: list of endpoints.LimitDefinition objects, quota metric
1051 definitions for this API.
1052 use_request_uri: if true, match requests against REQUEST_URI instead of PATH_INFO
1053
1054
1055 Returns:
1056 Class decorated with api_info attribute, an instance of ApiInfo.
1057 """
1058 if auth_level is not None:
1059 _logger.warn(_AUTH_LEVEL_WARNING)
1060
1061 return _ApiDecorator(name, version, description=description,
1062 hostname=hostname, audiences=audiences, scopes=scopes,
1063 allowed_client_ids=allowed_client_ids,
1064 canonical_name=canonical_name, auth=auth,
1065 owner_domain=owner_domain, owner_name=owner_name,
1066 package_path=package_path,
1067 frontend_limits=frontend_limits, title=title,
1068 documentation=documentation, auth_level=auth_level,
1069 issuers=issuers, namespace=namespace,
1070 api_key_required=api_key_required, base_path=base_path,
1071 limit_definitions=limit_definitions,
1072 use_request_uri=use_request_uri)
1073
1074
1075class _MethodInfo(object):
1076 """Configurable attributes of an API method.
1077
1078 Consolidates settings from @method decorator and/or any settings that were
1079 calculating from the ProtoRPC method name, so they only need to be calculated
1080 once.
1081 """
1082
1083 @util.positional(1)
1084 def __init__(self, name=None, path=None, http_method=None,
1085 scopes=None, audiences=None, allowed_client_ids=None,
1086 auth_level=None, api_key_required=None, request_body_class=None,
1087 request_params_class=None, metric_costs=None, use_request_uri=None):
1088 """Constructor.
1089
1090 Args:
1091 name: string, Name of the method, prepended with <apiname>. to make it
1092 unique.
1093 path: string, Path portion of the URL to the method, for RESTful methods.
1094 http_method: string, HTTP method supported by the method.
1095 scopes: list of string, OAuth2 token must contain one of these scopes.
1096 audiences: list of string, IdToken must contain one of these audiences.
1097 allowed_client_ids: list of string, Client IDs allowed to call the method.
1098 auth_level: enum from AUTH_LEVEL, Frontend auth level for the method.
1099 api_key_required: bool, whether a key is required to call the method.
1100 request_body_class: The type for the request body when using a
1101 ResourceContainer. Otherwise, null.
1102 request_params_class: The type for the request parameters when using a
1103 ResourceContainer. Otherwise, null.
1104 metric_costs: dict with keys matching an API limit metric and values
1105 representing the cost for each successful call against that metric.
1106 use_request_uri: if true, match requests against REQUEST_URI instead of PATH_INFO
1107 """
1108 self.__name = name
1109 self.__path = path
1110 self.__http_method = http_method
1111 self.__scopes = endpoints_types.OAuth2Scope.convert_list(scopes)
1112 self.__audiences = audiences
1113 self.__allowed_client_ids = allowed_client_ids
1114 self.__auth_level = auth_level
1115 self.__api_key_required = api_key_required
1116 self.__request_body_class = request_body_class
1117 self.__request_params_class = request_params_class
1118 self.__metric_costs = metric_costs
1119 self.__use_request_uri = use_request_uri
1120
1121 def __safe_name(self, method_name):
1122 """Restrict method name to a-zA-Z0-9_, first char lowercase."""
1123 # Endpoints backend restricts what chars are allowed in a method name.
1124 safe_name = re.sub(r'[^\.a-zA-Z0-9_]', '', method_name)
1125
1126 # Strip any number of leading underscores.
1127 safe_name = safe_name.lstrip('_')
1128
1129 # Ensure the first character is lowercase.
1130 # Slice from 0:1 rather than indexing [0] in case safe_name is length 0.
1131 return safe_name[0:1].lower() + safe_name[1:]
1132
1133 @property
1134 def name(self):
1135 """Method name as specified in decorator or derived."""
1136 return self.__name
1137
1138 def get_path(self, api_info):
1139 """Get the path portion of the URL to the method (for RESTful methods).
1140
1141 Request path can be specified in the method, and it could have a base
1142 path prepended to it.
1143
1144 Args:
1145 api_info: API information for this API, possibly including a base path.
1146 This is the api_info property on the class that's been annotated for
1147 this API.
1148
1149 Returns:
1150 This method's request path (not including the http://.../{base_path}
1151 prefix).
1152
1153 Raises:
1154 ApiConfigurationError: If the path isn't properly formatted.
1155 """
1156 path = self.__path or ''
1157 if path and path[0] == '/':
1158 # Absolute path, ignoring any prefixes. Just strip off the leading /.
1159 path = path[1:]
1160 else:
1161 # Relative path.
1162 if api_info.path:
1163 path = '%s%s%s' % (api_info.path, '/' if path else '', path)
1164
1165 # Verify that the path seems valid.
1166 parts = path.split('/')
1167 for n, part in enumerate(parts):
1168 r = _VALID_PART_RE if n < len(parts) - 1 else _VALID_LAST_PART_RE
1169 if part and '{' in part and '}' in part:
1170 if not r.match(part):
1171 raise api_exceptions.ApiConfigurationError(
1172 'Invalid path segment: %s (part of %s)' % (part, path))
1173 return path
1174
1175 @property
1176 def http_method(self):
1177 """HTTP method supported by the method (e.g. GET, POST)."""
1178 return self.__http_method
1179
1180 @property
1181 def scope_objs(self):
1182 """List of scopes (as OAuth2Scopes) accepted for the API method."""
1183 return self.__scopes
1184
1185 @property
1186 def scopes(self):
1187 """List of scopes (as strings) accepted for the API method."""
1188 if self.scope_objs is not None:
1189 return [_s.scope for _s in self.scope_objs]
1190
1191 @property
1192 def audiences(self):
1193 """List of audiences for the API method."""
1194 return self.__audiences
1195
1196 @property
1197 def allowed_client_ids(self):
1198 """List of allowed client IDs for the API method."""
1199 return self.__allowed_client_ids
1200
1201 @property
1202 def auth_level(self):
1203 """Enum from AUTH_LEVEL specifying default frontend auth level."""
1204 return self.__auth_level
1205
1206 @property
1207 def api_key_required(self):
1208 """bool whether a key is required to call the API method."""
1209 return self.__api_key_required
1210
1211 @property
1212 def metric_costs(self):
1213 """Dict mapping API limit metric names to costs against that metric."""
1214 return self.__metric_costs
1215
1216 @property
1217 def request_body_class(self):
1218 """Type of request body when using a ResourceContainer."""
1219 return self.__request_body_class
1220
1221 @property
1222 def request_params_class(self):
1223 """Type of request parameter message when using a ResourceContainer."""
1224 return self.__request_params_class
1225
1226 def is_api_key_required(self, api_info):
1227 if self.api_key_required is not None:
1228 return self.api_key_required
1229 else:
1230 return api_info.api_key_required
1231
1232 def use_request_uri(self, api_info):
1233 if self.__use_request_uri is not None:
1234 return self.__use_request_uri
1235 else:
1236 return api_info.use_request_uri
1237
1238 def method_id(self, api_info):
1239 """Computed method name."""
1240 # This is done here for now because at __init__ time, the method is known
1241 # but not the api, and thus not the api name. Later, in
1242 # ApiConfigGenerator.__method_descriptor, the api name is known.
1243 if api_info.resource_name:
1244 resource_part = '.%s' % self.__safe_name(api_info.resource_name)
1245 else:
1246 resource_part = ''
1247 return '%s%s.%s' % (self.__safe_name(api_info.name), resource_part,
1248 self.__safe_name(self.name))
1249
1250
1251@util.positional(2)
1252def method(request_message=message_types.VoidMessage,
1253 response_message=message_types.VoidMessage,
1254 name=None,
1255 path=None,
1256 http_method='POST',
1257 scopes=None,
1258 audiences=None,
1259 allowed_client_ids=None,
1260 auth_level=None,
1261 api_key_required=None,
1262 metric_costs=None,
1263 use_request_uri=None):
1264 """Decorate a ProtoRPC Method for use by the framework above.
1265
1266 This decorator can be used to specify a method name, path, http method,
1267 scopes, audiences, client ids and auth_level.
1268
1269 Sample usage:
1270 @api_config.method(RequestMessage, ResponseMessage,
1271 name='insert', http_method='PUT')
1272 def greeting_insert(request):
1273 ...
1274 return response
1275
1276 Args:
1277 request_message: Message type of expected request.
1278 response_message: Message type of expected response.
1279 name: string, Name of the method, prepended with <apiname>. to make it
1280 unique. (Default: python method name)
1281 path: string, Path portion of the URL to the method, for RESTful methods.
1282 http_method: string, HTTP method supported by the method. (Default: POST)
1283 scopes: list of string, OAuth2 token must contain one of these scopes.
1284 audiences: list of string, IdToken must contain one of these audiences.
1285 allowed_client_ids: list of string, Client IDs allowed to call the method.
1286 If None and auth_level is REQUIRED, no calls will be allowed.
1287 auth_level: enum from AUTH_LEVEL, Frontend auth level for the method.
1288 api_key_required: bool, whether a key is required to call the method
1289 metric_costs: dict with keys matching an API limit metric and values
1290 representing the cost for each successful call against that metric.
1291 use_request_uri: if true, match requests against REQUEST_URI instead of PATH_INFO
1292
1293 Returns:
1294 'apiserving_method_wrapper' function.
1295
1296 Raises:
1297 TypeError: if the request_type or response_type parameters are not
1298 proper subclasses of messages.Message.
1299 """
1300 if auth_level is not None:
1301 _logger.warn(_AUTH_LEVEL_WARNING)
1302
1303 # Default HTTP method if one is not specified.
1304 DEFAULT_HTTP_METHOD = 'POST'
1305
1306 def apiserving_method_decorator(api_method):
1307 """Decorator for ProtoRPC method that configures Google's API server.
1308
1309 Args:
1310 api_method: Original method being wrapped.
1311
1312 Returns:
1313 Function responsible for actual invocation.
1314 Assigns the following attributes to invocation function:
1315 remote: Instance of RemoteInfo, contains remote method information.
1316 remote.request_type: Expected request type for remote method.
1317 remote.response_type: Response type returned from remote method.
1318 method_info: Instance of _MethodInfo, api method configuration.
1319 It is also assigned attributes corresponding to the aforementioned kwargs.
1320
1321 Raises:
1322 TypeError: if the request_type or response_type parameters are not
1323 proper subclasses of messages.Message.
1324 KeyError: if the request_message is a ResourceContainer and the newly
1325 created remote method has been reference by the container before. This
1326 should never occur because a remote method is created once.
1327 """
1328 request_body_class = None
1329 request_params_class = None
1330 if isinstance(request_message, resource_container.ResourceContainer):
1331 remote_decorator = remote.method(request_message.combined_message_class,
1332 response_message)
1333 request_body_class = request_message.body_message_class()
1334 request_params_class = request_message.parameters_message_class()
1335 else:
1336 remote_decorator = remote.method(request_message, response_message)
1337 remote_method = remote_decorator(api_method)
1338
1339 def invoke_remote(service_instance, request):
1340 # If the server didn't specify any auth information, build it now.
1341 # pylint: disable=protected-access
1342 users_id_token._maybe_set_current_user_vars(
1343 invoke_remote, api_info=getattr(service_instance, 'api_info', None),
1344 request=request)
1345 # pylint: enable=protected-access
1346 return remote_method(service_instance, request)
1347
1348 invoke_remote.remote = remote_method.remote
1349 if isinstance(request_message, resource_container.ResourceContainer):
1350 resource_container.ResourceContainer.add_to_cache(
1351 invoke_remote.remote, request_message)
1352
1353 invoke_remote.method_info = _MethodInfo(
1354 name=name or api_method.__name__, path=path or api_method.__name__,
1355 http_method=http_method or DEFAULT_HTTP_METHOD,
1356 scopes=scopes, audiences=audiences,
1357 allowed_client_ids=allowed_client_ids, auth_level=auth_level,
1358 api_key_required=api_key_required, metric_costs=metric_costs,
1359 use_request_uri=use_request_uri,
1360 request_body_class=request_body_class,
1361 request_params_class=request_params_class)
1362 invoke_remote.__name__ = invoke_remote.method_info.name
1363 return invoke_remote
1364
1365 endpoints_util.check_list_type(scopes, (six.string_types, endpoints_types.OAuth2Scope), 'scopes')
1366 endpoints_util.check_list_type(allowed_client_ids, six.string_types,
1367 'allowed_client_ids')
1368 _CheckEnum(auth_level, AUTH_LEVEL, 'auth_level')
1369
1370 _CheckAudiences(audiences)
1371
1372 _CheckType(metric_costs, dict, 'metric_costs')
1373
1374 return apiserving_method_decorator
1375
1376
1377class ApiConfigGenerator(object):
1378 """Generates an API configuration from a ProtoRPC service.
1379
1380 Example:
1381
1382 class HelloRequest(messages.Message):
1383 my_name = messages.StringField(1, required=True)
1384
1385 class HelloResponse(messages.Message):
1386 hello = messages.StringField(1, required=True)
1387
1388 class HelloService(remote.Service):
1389
1390 @remote.method(HelloRequest, HelloResponse)
1391 def hello(self, request):
1392 return HelloResponse(hello='Hello there, %s!' %
1393 request.my_name)
1394
1395 api_config = ApiConfigGenerator().pretty_print_config_to_json(HelloService)
1396
1397 The resulting api_config will be a JSON document describing the API
1398 implemented by HelloService.
1399 """
1400
1401 # Constants for categorizing a request method.
1402 # __NO_BODY - Request without a request body, such as GET and DELETE methods.
1403 # __HAS_BODY - Request (such as POST/PUT/PATCH) with info in the request body.
1404 __NO_BODY = 1
1405 __HAS_BODY = 2
1406
1407 def __init__(self):
1408 self.__parser = message_parser.MessageTypeToJsonSchema()
1409
1410 # Maps method id to the request schema id.
1411 self.__request_schema = {}
1412
1413 # Maps method id to the response schema id.
1414 self.__response_schema = {}
1415
1416 # Maps from ProtoRPC name to method id.
1417 self.__id_from_name = {}
1418
1419 def __get_request_kind(self, method_info):
1420 """Categorize the type of the request.
1421
1422 Args:
1423 method_info: _MethodInfo, method information.
1424
1425 Returns:
1426 The kind of request.
1427 """
1428 if method_info.http_method in ('GET', 'DELETE'):
1429 return self.__NO_BODY
1430 else:
1431 return self.__HAS_BODY
1432
1433 def __field_to_subfields(self, field):
1434 """Fully describes data represented by field, including the nested case.
1435
1436 In the case that the field is not a message field, we have no fields nested
1437 within a message definition, so we can simply return that field. However, in
1438 the nested case, we can't simply describe the data with one field or even
1439 with one chain of fields.
1440
1441 For example, if we have a message field
1442
1443 m_field = messages.MessageField(RefClass, 1)
1444
1445 which references a class with two fields:
1446
1447 class RefClass(messages.Message):
1448 one = messages.StringField(1)
1449 two = messages.IntegerField(2)
1450
1451 then we would need to include both one and two to represent all the
1452 data contained.
1453
1454 Calling __field_to_subfields(m_field) would return:
1455 [
1456 [<MessageField "m_field">, <StringField "one">],
1457 [<MessageField "m_field">, <StringField "two">],
1458 ]
1459
1460 If the second field was instead a message field
1461
1462 class RefClass(messages.Message):
1463 one = messages.StringField(1)
1464 two = messages.MessageField(OtherRefClass, 2)
1465
1466 referencing another class with two fields
1467
1468 class OtherRefClass(messages.Message):
1469 three = messages.BooleanField(1)
1470 four = messages.FloatField(2)
1471
1472 then we would need to recurse one level deeper for two.
1473
1474 With this change, calling __field_to_subfields(m_field) would return:
1475 [
1476 [<MessageField "m_field">, <StringField "one">],
1477 [<MessageField "m_field">, <StringField "two">, <StringField "three">],
1478 [<MessageField "m_field">, <StringField "two">, <StringField "four">],
1479 ]
1480
1481 Args:
1482 field: An instance of a subclass of messages.Field.
1483
1484 Returns:
1485 A list of lists, where each sublist is a list of fields.
1486 """
1487 # Termination condition
1488 if not isinstance(field, messages.MessageField):
1489 return [[field]]
1490
1491 result = []
1492 for subfield in sorted(field.message_type.all_fields(),
1493 key=lambda f: f.number):
1494 subfield_results = self.__field_to_subfields(subfield)
1495 for subfields_list in subfield_results:
1496 subfields_list.insert(0, field)
1497 result.append(subfields_list)
1498 return result
1499
1500 # TODO(dhermes): Support all the parameter types
1501 # Currently missing DATE and ETAG
1502 def __field_to_parameter_type(self, field):
1503 """Converts the field variant type into a string describing the parameter.
1504
1505 Args:
1506 field: An instance of a subclass of messages.Field.
1507
1508 Returns:
1509 A string corresponding to the variant enum of the field, with a few
1510 exceptions. In the case of signed ints, the 's' is dropped; for the BOOL
1511 variant, 'boolean' is used; and for the ENUM variant, 'string' is used.
1512
1513 Raises:
1514 TypeError: if the field variant is a message variant.
1515 """
1516 # We use lowercase values for types (e.g. 'string' instead of 'STRING').
1517 variant = field.variant
1518 if variant == messages.Variant.MESSAGE:
1519 raise TypeError('A message variant can\'t be used in a parameter.')
1520
1521 custom_variant_map = {
1522 messages.Variant.SINT32: 'int32',
1523 messages.Variant.SINT64: 'int64',
1524 messages.Variant.BOOL: 'boolean',
1525 messages.Variant.ENUM: 'string',
1526 }
1527 return custom_variant_map.get(variant) or variant.name.lower()
1528
1529 def __get_path_parameters(self, path):
1530 """Parses path paremeters from a URI path and organizes them by parameter.
1531
1532 Some of the parameters may correspond to message fields, and so will be
1533 represented as segments corresponding to each subfield; e.g. first.second if
1534 the field "second" in the message field "first" is pulled from the path.
1535
1536 The resulting dictionary uses the first segments as keys and each key has as
1537 value the list of full parameter values with first segment equal to the key.
1538
1539 If the match path parameter is null, that part of the path template is
1540 ignored; this occurs if '{}' is used in a template.
1541
1542 Args:
1543 path: String; a URI path, potentially with some parameters.
1544
1545 Returns:
1546 A dictionary with strings as keys and list of strings as values.
1547 """
1548 path_parameters_by_segment = {}
1549 for format_var_name in re.findall(_PATH_VARIABLE_PATTERN, path):
1550 first_segment = format_var_name.split('.', 1)[0]
1551 matches = path_parameters_by_segment.setdefault(first_segment, [])
1552 matches.append(format_var_name)
1553
1554 return path_parameters_by_segment
1555
1556 def __validate_simple_subfield(self, parameter, field, segment_list,
1557 _segment_index=0):
1558 """Verifies that a proposed subfield actually exists and is a simple field.
1559
1560 Here, simple means it is not a MessageField (nested).
1561
1562 Args:
1563 parameter: String; the '.' delimited name of the current field being
1564 considered. This is relative to some root.
1565 field: An instance of a subclass of messages.Field. Corresponds to the
1566 previous segment in the path (previous relative to _segment_index),
1567 since this field should be a message field with the current segment
1568 as a field in the message class.
1569 segment_list: The full list of segments from the '.' delimited subfield
1570 being validated.
1571 _segment_index: Integer; used to hold the position of current segment so
1572 that segment_list can be passed as a reference instead of having to
1573 copy using segment_list[1:] at each step.
1574
1575 Raises:
1576 TypeError: If the final subfield (indicated by _segment_index relative
1577 to the length of segment_list) is a MessageField.
1578 TypeError: If at any stage the lookup at a segment fails, e.g if a.b
1579 exists but a.b.c does not exist. This can happen either if a.b is not
1580 a message field or if a.b.c is not a property on the message class from
1581 a.b.
1582 """
1583 if _segment_index >= len(segment_list):
1584 # In this case, the field is the final one, so should be simple type
1585 if isinstance(field, messages.MessageField):
1586 field_class = field.__class__.__name__
1587 raise TypeError('Can\'t use messages in path. Subfield %r was '
1588 'included but is a %s.' % (parameter, field_class))
1589 return
1590
1591 segment = segment_list[_segment_index]
1592 parameter += '.' + segment
1593 try:
1594 field = field.type.field_by_name(segment)
1595 except (AttributeError, KeyError):
1596 raise TypeError('Subfield %r from path does not exist.' % (parameter,))
1597
1598 self.__validate_simple_subfield(parameter, field, segment_list,
1599 _segment_index=_segment_index + 1)
1600
1601 def __validate_path_parameters(self, field, path_parameters):
1602 """Verifies that all path parameters correspond to an existing subfield.
1603
1604 Args:
1605 field: An instance of a subclass of messages.Field. Should be the root
1606 level property name in each path parameter in path_parameters. For
1607 example, if the field is called 'foo', then each path parameter should
1608 begin with 'foo.'.
1609 path_parameters: A list of Strings representing URI parameter variables.
1610
1611 Raises:
1612 TypeError: If one of the path parameters does not start with field.name.
1613 """
1614 for param in path_parameters:
1615 segment_list = param.split('.')
1616 if segment_list[0] != field.name:
1617 raise TypeError('Subfield %r can\'t come from field %r.'
1618 % (param, field.name))
1619 self.__validate_simple_subfield(field.name, field, segment_list[1:])
1620
1621 def __parameter_default(self, final_subfield):
1622 """Returns default value of final subfield if it has one.
1623
1624 If this subfield comes from a field list returned from __field_to_subfields,
1625 none of the fields in the subfield list can have a default except the final
1626 one since they all must be message fields.
1627
1628 Args:
1629 final_subfield: A simple field from the end of a subfield list.
1630
1631 Returns:
1632 The default value of the subfield, if any exists, with the exception of an
1633 enum field, which will have its value cast to a string.
1634 """
1635 if final_subfield.default:
1636 if isinstance(final_subfield, messages.EnumField):
1637 return final_subfield.default.name
1638 else:
1639 return final_subfield.default
1640
1641 def __parameter_enum(self, final_subfield):
1642 """Returns enum descriptor of final subfield if it is an enum.
1643
1644 An enum descriptor is a dictionary with keys as the names from the enum and
1645 each value is a dictionary with a single key "backendValue" and value equal
1646 to the same enum name used to stored it in the descriptor.
1647
1648 The key "description" can also be used next to "backendValue", but protorpc
1649 Enum classes have no way of supporting a description for each value.
1650
1651 Args:
1652 final_subfield: A simple field from the end of a subfield list.
1653
1654 Returns:
1655 The enum descriptor for the field, if it's an enum descriptor, else
1656 returns None.
1657 """
1658 if isinstance(final_subfield, messages.EnumField):
1659 enum_descriptor = {}
1660 for enum_value in final_subfield.type.to_dict().keys():
1661 enum_descriptor[enum_value] = {'backendValue': enum_value}
1662 return enum_descriptor
1663
1664 def __parameter_descriptor(self, subfield_list):
1665 """Creates descriptor for a parameter using the subfields that define it.
1666
1667 Each parameter is defined by a list of fields, with all but the last being
1668 a message field and the final being a simple (non-message) field.
1669
1670 Many of the fields in the descriptor are determined solely by the simple
1671 field at the end, though some (such as repeated and required) take the whole
1672 chain of fields into consideration.
1673
1674 Args:
1675 subfield_list: List of fields describing the parameter.
1676
1677 Returns:
1678 Dictionary containing a descriptor for the parameter described by the list
1679 of fields.
1680 """
1681 descriptor = {}
1682 final_subfield = subfield_list[-1]
1683
1684 # Required
1685 if all(subfield.required for subfield in subfield_list):
1686 descriptor['required'] = True
1687
1688 # Type
1689 descriptor['type'] = self.__field_to_parameter_type(final_subfield)
1690
1691 # Default
1692 default = self.__parameter_default(final_subfield)
1693 if default is not None:
1694 descriptor['default'] = default
1695
1696 # Repeated
1697 if any(subfield.repeated for subfield in subfield_list):
1698 descriptor['repeated'] = True
1699
1700 # Enum
1701 enum_descriptor = self.__parameter_enum(final_subfield)
1702 if enum_descriptor is not None:
1703 descriptor['enum'] = enum_descriptor
1704
1705 return descriptor
1706
1707 def __add_parameters_from_field(self, field, path_parameters,
1708 params, param_order):
1709 """Adds all parameters in a field to a method parameters descriptor.
1710
1711 Simple fields will only have one parameter, but a message field 'x' that
1712 corresponds to a message class with fields 'y' and 'z' will result in
1713 parameters 'x.y' and 'x.z', for example. The mapping from field to
1714 parameters is mostly handled by __field_to_subfields.
1715
1716 Args:
1717 field: Field from which parameters will be added to the method descriptor.
1718 path_parameters: A list of parameters matched from a path for this field.
1719 For example for the hypothetical 'x' from above if the path was
1720 '/a/{x.z}/b/{other}' then this list would contain only the element
1721 'x.z' since 'other' does not match to this field.
1722 params: Dictionary with parameter names as keys and parameter descriptors
1723 as values. This will be updated for each parameter in the field.
1724 param_order: List of required parameter names to give them an order in the
1725 descriptor. All required parameters in the field will be added to this
1726 list.
1727 """
1728 for subfield_list in self.__field_to_subfields(field):
1729 descriptor = self.__parameter_descriptor(subfield_list)
1730
1731 qualified_name = '.'.join(subfield.name for subfield in subfield_list)
1732 in_path = qualified_name in path_parameters
1733 if descriptor.get('required', in_path):
1734 descriptor['required'] = True
1735 param_order.append(qualified_name)
1736
1737 params[qualified_name] = descriptor
1738
1739 def __params_descriptor_without_container(self, message_type,
1740 request_kind, path):
1741 """Describe parameters of a method which does not use a ResourceContainer.
1742
1743 Makes sure that the path parameters are included in the message definition
1744 and adds any required fields and URL query parameters.
1745
1746 This method is to preserve backwards compatibility and will be removed in
1747 a future release.
1748
1749 Args:
1750 message_type: messages.Message class, Message with parameters to describe.
1751 request_kind: The type of request being made.
1752 path: string, HTTP path to method.
1753
1754 Returns:
1755 A tuple (dict, list of string): Descriptor of the parameters, Order of the
1756 parameters.
1757 """
1758 params = {}
1759 param_order = []
1760
1761 path_parameter_dict = self.__get_path_parameters(path)
1762 for field in sorted(message_type.all_fields(), key=lambda f: f.number):
1763 matched_path_parameters = path_parameter_dict.get(field.name, [])
1764 self.__validate_path_parameters(field, matched_path_parameters)
1765 if matched_path_parameters or request_kind == self.__NO_BODY:
1766 self.__add_parameters_from_field(field, matched_path_parameters,
1767 params, param_order)
1768
1769 return params, param_order
1770
1771 # TODO(user): request_kind is only used by
1772 # __params_descriptor_without_container so can be removed
1773 # once that method is fully deprecated.
1774 def __params_descriptor(self, message_type, request_kind, path, method_id):
1775 """Describe the parameters of a method.
1776
1777 If the message_type is not a ResourceContainer, will fall back to
1778 __params_descriptor_without_container (which will eventually be deprecated).
1779
1780 If the message type is a ResourceContainer, then all path/query parameters
1781 will come from the ResourceContainer This method will also make sure all
1782 path parameters are covered by the message fields.
1783
1784 Args:
1785 message_type: messages.Message or ResourceContainer class, Message with
1786 parameters to describe.
1787 request_kind: The type of request being made.
1788 path: string, HTTP path to method.
1789 method_id: string, Unique method identifier (e.g. 'myapi.items.method')
1790
1791 Returns:
1792 A tuple (dict, list of string): Descriptor of the parameters, Order of the
1793 parameters.
1794 """
1795 path_parameter_dict = self.__get_path_parameters(path)
1796
1797 if not isinstance(message_type, resource_container.ResourceContainer):
1798 if path_parameter_dict:
1799 _logger.warning('Method %s specifies path parameters but you are not '
1800 'using a ResourceContainer; instead, you are using %r. '
1801 'This will fail in future releases; please switch to '
1802 'using ResourceContainer as soon as possible.',
1803 method_id, type(message_type))
1804 return self.__params_descriptor_without_container(
1805 message_type, request_kind, path)
1806
1807 # From here, we can assume message_type is a ResourceContainer
1808 message_type = message_type.parameters_message_class()
1809
1810 params = {}
1811 param_order = []
1812
1813 # Make sure all path parameters are covered.
1814 for field_name, matched_path_parameters in path_parameter_dict.items():
1815 field = message_type.field_by_name(field_name)
1816 self.__validate_path_parameters(field, matched_path_parameters)
1817
1818 # Add all fields, sort by field.number since we have parameterOrder.
1819 for field in sorted(message_type.all_fields(), key=lambda f: f.number):
1820 matched_path_parameters = path_parameter_dict.get(field.name, [])
1821 self.__add_parameters_from_field(field, matched_path_parameters,
1822 params, param_order)
1823
1824 return params, param_order
1825
1826 def __request_message_descriptor(self, request_kind, message_type, method_id,
1827 path):
1828 """Describes the parameters and body of the request.
1829
1830 Args:
1831 request_kind: The type of request being made.
1832 message_type: messages.Message or ResourceContainer class. The message to
1833 describe.
1834 method_id: string, Unique method identifier (e.g. 'myapi.items.method')
1835 path: string, HTTP path to method.
1836
1837 Returns:
1838 Dictionary describing the request.
1839
1840 Raises:
1841 ValueError: if the method path and request required fields do not match
1842 """
1843 descriptor = {}
1844
1845 params, param_order = self.__params_descriptor(message_type, request_kind,
1846 path, method_id)
1847
1848 if isinstance(message_type, resource_container.ResourceContainer):
1849 message_type = message_type.body_message_class()
1850
1851 if (request_kind == self.__NO_BODY or
1852 message_type == message_types.VoidMessage()):
1853 descriptor['body'] = 'empty'
1854 else:
1855 descriptor['body'] = 'autoTemplate(backendRequest)'
1856 descriptor['bodyName'] = 'resource'
1857 self.__request_schema[method_id] = self.__parser.add_message(
1858 message_type.__class__)
1859
1860 if params:
1861 descriptor['parameters'] = params
1862
1863 if param_order:
1864 descriptor['parameterOrder'] = param_order
1865
1866 return descriptor
1867
1868 def __response_message_descriptor(self, message_type, method_id):
1869 """Describes the response.
1870
1871 Args:
1872 message_type: messages.Message class, The message to describe.
1873 method_id: string, Unique method identifier (e.g. 'myapi.items.method')
1874
1875 Returns:
1876 Dictionary describing the response.
1877 """
1878 descriptor = {}
1879
1880 self.__parser.add_message(message_type.__class__)
1881 if message_type == message_types.VoidMessage():
1882 descriptor['body'] = 'empty'
1883 else:
1884 descriptor['body'] = 'autoTemplate(backendResponse)'
1885 descriptor['bodyName'] = 'resource'
1886 self.__response_schema[method_id] = self.__parser.ref_for_message_type(
1887 message_type.__class__)
1888
1889 return descriptor
1890
1891 def __method_descriptor(self, service, method_info,
1892 rosy_method, protorpc_method_info):
1893 """Describes a method.
1894
1895 Args:
1896 service: endpoints.Service, Implementation of the API as a service.
1897 method_info: _MethodInfo, Configuration for the method.
1898 rosy_method: string, ProtoRPC method name prefixed with the
1899 name of the service.
1900 protorpc_method_info: protorpc.remote._RemoteMethodInfo, ProtoRPC
1901 description of the method.
1902
1903 Returns:
1904 Dictionary describing the method.
1905 """
1906 descriptor = {}
1907
1908 request_message_type = (resource_container.ResourceContainer.
1909 get_request_message(protorpc_method_info.remote))
1910 request_kind = self.__get_request_kind(method_info)
1911 remote_method = protorpc_method_info.remote
1912
1913 descriptor['path'] = method_info.get_path(service.api_info)
1914 descriptor['httpMethod'] = method_info.http_method
1915 descriptor['rosyMethod'] = rosy_method
1916 descriptor['request'] = self.__request_message_descriptor(
1917 request_kind, request_message_type,
1918 method_info.method_id(service.api_info),
1919 descriptor['path'])
1920 descriptor['response'] = self.__response_message_descriptor(
1921 remote_method.response_type(), method_info.method_id(service.api_info))
1922
1923 # Audiences, scopes, allowed_client_ids and auth_level could be set at
1924 # either the method level or the API level. Allow an empty list at the
1925 # method level to override the setting at the API level.
1926 scopes = (method_info.scopes
1927 if method_info.scopes is not None
1928 else service.api_info.scopes)
1929 if scopes:
1930 descriptor['scopes'] = scopes
1931 audiences = (method_info.audiences
1932 if method_info.audiences is not None
1933 else service.api_info.audiences)
1934 if audiences:
1935 descriptor['audiences'] = audiences
1936 allowed_client_ids = (method_info.allowed_client_ids
1937 if method_info.allowed_client_ids is not None
1938 else service.api_info.allowed_client_ids)
1939 if allowed_client_ids:
1940 descriptor['clientIds'] = allowed_client_ids
1941
1942 if remote_method.method.__doc__:
1943 descriptor['description'] = remote_method.method.__doc__
1944
1945 auth_level = (method_info.auth_level
1946 if method_info.auth_level is not None
1947 else service.api_info.auth_level)
1948 if auth_level is not None:
1949 descriptor['authLevel'] = AUTH_LEVEL.reverse_mapping[auth_level]
1950
1951 descriptor['useRequestUri'] = method_info.use_request_uri(service.api_info)
1952
1953 return descriptor
1954
1955 def __schema_descriptor(self, services):
1956 """Descriptor for the all the JSON Schema used.
1957
1958 Args:
1959 services: List of protorpc.remote.Service instances implementing an
1960 api/version.
1961
1962 Returns:
1963 Dictionary containing all the JSON Schema used in the service.
1964 """
1965 methods_desc = {}
1966
1967 for service in services:
1968 protorpc_methods = service.all_remote_methods()
1969 for protorpc_method_name in protorpc_methods.keys():
1970 rosy_method = '%s.%s' % (service.__name__, protorpc_method_name)
1971 method_id = self.__id_from_name[rosy_method]
1972
1973 request_response = {}
1974
1975 request_schema_id = self.__request_schema.get(method_id)
1976 if request_schema_id:
1977 request_response['request'] = {
1978 '$ref': request_schema_id
1979 }
1980
1981 response_schema_id = self.__response_schema.get(method_id)
1982 if response_schema_id:
1983 request_response['response'] = {
1984 '$ref': response_schema_id
1985 }
1986
1987 methods_desc[rosy_method] = request_response
1988
1989 descriptor = {
1990 'methods': methods_desc,
1991 'schemas': self.__parser.schemas(),
1992 }
1993
1994 return descriptor
1995
1996 def __get_merged_api_info(self, services):
1997 """Builds a description of an API.
1998
1999 Args:
2000 services: List of protorpc.remote.Service instances implementing an
2001 api/version.
2002
2003 Returns:
2004 The _ApiInfo object to use for the API that the given services implement.
2005
2006 Raises:
2007 ApiConfigurationError: If there's something wrong with the API
2008 configuration, such as a multiclass API decorated with different API
2009 descriptors (see the docstring for api()).
2010 """
2011 merged_api_info = services[0].api_info
2012
2013 # Verify that, if there are multiple classes here, they're allowed to
2014 # implement the same API.
2015 for service in services[1:]:
2016 if not merged_api_info.is_same_api(service.api_info):
2017 raise api_exceptions.ApiConfigurationError(
2018 _MULTICLASS_MISMATCH_ERROR_TEMPLATE % (service.api_info.name,
2019 service.api_info.api_version))
2020
2021 return merged_api_info
2022
2023 def __auth_descriptor(self, api_info):
2024 """Builds an auth descriptor from API info.
2025
2026 Args:
2027 api_info: An _ApiInfo object.
2028
2029 Returns:
2030 A dictionary with 'allowCookieAuth' and/or 'blockedRegions' keys.
2031 """
2032 if api_info.auth is None:
2033 return None
2034
2035 auth_descriptor = {}
2036 if api_info.auth.allow_cookie_auth is not None:
2037 auth_descriptor['allowCookieAuth'] = api_info.auth.allow_cookie_auth
2038 if api_info.auth.blocked_regions:
2039 auth_descriptor['blockedRegions'] = api_info.auth.blocked_regions
2040
2041 return auth_descriptor
2042
2043 def __frontend_limit_descriptor(self, api_info):
2044 """Builds a frontend limit descriptor from API info.
2045
2046 Args:
2047 api_info: An _ApiInfo object.
2048
2049 Returns:
2050 A dictionary with frontend limit information.
2051 """
2052 if api_info.frontend_limits is None:
2053 return None
2054
2055 descriptor = {}
2056 for propname, descname in (('unregistered_user_qps', 'unregisteredUserQps'),
2057 ('unregistered_qps', 'unregisteredQps'),
2058 ('unregistered_daily', 'unregisteredDaily')):
2059 if getattr(api_info.frontend_limits, propname) is not None:
2060 descriptor[descname] = getattr(api_info.frontend_limits, propname)
2061
2062 rules = self.__frontend_limit_rules_descriptor(api_info)
2063 if rules:
2064 descriptor['rules'] = rules
2065
2066 return descriptor
2067
2068 def __frontend_limit_rules_descriptor(self, api_info):
2069 """Builds a frontend limit rules descriptor from API info.
2070
2071 Args:
2072 api_info: An _ApiInfo object.
2073
2074 Returns:
2075 A list of dictionaries with frontend limit rules information.
2076 """
2077 if not api_info.frontend_limits.rules:
2078 return None
2079
2080 rules = []
2081 for rule in api_info.frontend_limits.rules:
2082 descriptor = {}
2083 for propname, descname in (('match', 'match'),
2084 ('qps', 'qps'),
2085 ('user_qps', 'userQps'),
2086 ('daily', 'daily'),
2087 ('analytics_id', 'analyticsId')):
2088 if getattr(rule, propname) is not None:
2089 descriptor[descname] = getattr(rule, propname)
2090 if descriptor:
2091 rules.append(descriptor)
2092
2093 return rules
2094
2095 def __api_descriptor(self, services, hostname=None):
2096 """Builds a description of an API.
2097
2098 Args:
2099 services: List of protorpc.remote.Service instances implementing an
2100 api/version.
2101 hostname: string, Hostname of the API, to override the value set on the
2102 current service. Defaults to None.
2103
2104 Returns:
2105 A dictionary that can be deserialized into JSON and stored as an API
2106 description document.
2107
2108 Raises:
2109 ApiConfigurationError: If there's something wrong with the API
2110 configuration, such as a multiclass API decorated with different API
2111 descriptors (see the docstring for api()), or a repeated method
2112 signature.
2113 """
2114 merged_api_info = self.__get_merged_api_info(services)
2115 descriptor = self.get_descriptor_defaults(merged_api_info,
2116 hostname=hostname)
2117 description = merged_api_info.description
2118 if not description and len(services) == 1:
2119 description = services[0].__doc__
2120 if description:
2121 descriptor['description'] = description
2122
2123 auth_descriptor = self.__auth_descriptor(merged_api_info)
2124 if auth_descriptor:
2125 descriptor['auth'] = auth_descriptor
2126
2127 frontend_limit_descriptor = self.__frontend_limit_descriptor(
2128 merged_api_info)
2129 if frontend_limit_descriptor:
2130 descriptor['frontendLimits'] = frontend_limit_descriptor
2131
2132 method_map = {}
2133 method_collision_tracker = {}
2134 rest_collision_tracker = {}
2135
2136 for service in services:
2137 remote_methods = service.all_remote_methods()
2138 for protorpc_meth_name, protorpc_meth_info in remote_methods.items():
2139 method_info = getattr(protorpc_meth_info, 'method_info', None)
2140 # Skip methods that are not decorated with @method
2141 if method_info is None:
2142 continue
2143 method_id = method_info.method_id(service.api_info)
2144 rosy_method = '%s.%s' % (service.__name__, protorpc_meth_name)
2145 self.__id_from_name[rosy_method] = method_id
2146 method_map[method_id] = self.__method_descriptor(
2147 service, method_info, rosy_method, protorpc_meth_info)
2148
2149 # Make sure the same method name isn't repeated.
2150 if method_id in method_collision_tracker:
2151 raise api_exceptions.ApiConfigurationError(
2152 'Method %s used multiple times, in classes %s and %s' %
2153 (method_id, method_collision_tracker[method_id],
2154 service.__name__))
2155 else:
2156 method_collision_tracker[method_id] = service.__name__
2157
2158 # Make sure the same HTTP method & path aren't repeated.
2159 rest_identifier = (method_info.http_method,
2160 method_info.get_path(service.api_info))
2161 if rest_identifier in rest_collision_tracker:
2162 raise api_exceptions.ApiConfigurationError(
2163 '%s path "%s" used multiple times, in classes %s and %s' %
2164 (method_info.http_method, method_info.get_path(service.api_info),
2165 rest_collision_tracker[rest_identifier],
2166 service.__name__))
2167 else:
2168 rest_collision_tracker[rest_identifier] = service.__name__
2169
2170 if method_map:
2171 descriptor['methods'] = method_map
2172 descriptor['descriptor'] = self.__schema_descriptor(services)
2173
2174 return descriptor
2175
2176 def get_descriptor_defaults(self, api_info, hostname=None):
2177 """Gets a default configuration for a service.
2178
2179 Args:
2180 api_info: _ApiInfo object for this service.
2181 hostname: string, Hostname of the API, to override the value set on the
2182 current service. Defaults to None.
2183
2184 Returns:
2185 A dictionary with the default configuration.
2186 """
2187 hostname = (hostname or endpoints_util.get_app_hostname() or
2188 api_info.hostname)
2189 protocol = 'http' if ((hostname and hostname.startswith('localhost')) or
2190 endpoints_util.is_running_on_devserver()) else 'https'
2191 base_path = api_info.base_path.strip('/')
2192 defaults = {
2193 'extends': 'thirdParty.api',
2194 'root': '{0}://{1}/{2}'.format(protocol, hostname, base_path),
2195 'name': api_info.name,
2196 'version': api_info.api_version,
2197 'api_version': api_info.api_version,
2198 'path_version': api_info.path_version,
2199 'defaultVersion': True,
2200 'abstract': False,
2201 'adapter': {
2202 'bns': '{0}://{1}/{2}'.format(protocol, hostname, base_path),
2203 'type': 'lily',
2204 'deadline': 10.0
2205 }
2206 }
2207 if api_info.canonical_name:
2208 defaults['canonicalName'] = api_info.canonical_name
2209 if api_info.owner_domain:
2210 defaults['ownerDomain'] = api_info.owner_domain
2211 if api_info.owner_name:
2212 defaults['ownerName'] = api_info.owner_name
2213 if api_info.package_path:
2214 defaults['packagePath'] = api_info.package_path
2215 if api_info.title:
2216 defaults['title'] = api_info.title
2217 if api_info.documentation:
2218 defaults['documentation'] = api_info.documentation
2219 return defaults
2220
2221 def get_config_dict(self, services, hostname=None):
2222 """JSON dict description of a protorpc.remote.Service in API format.
2223
2224 Args:
2225 services: Either a single protorpc.remote.Service or a list of them
2226 that implements an api/version.
2227 hostname: string, Hostname of the API, to override the value set on the
2228 current service. Defaults to None.
2229
2230 Returns:
2231 dict, The API descriptor document as a JSON dict.
2232 """
2233 if not isinstance(services, (tuple, list)):
2234 services = [services]
2235 # The type of a class that inherits from remote.Service is actually
2236 # remote._ServiceClass, thanks to metaclass strangeness.
2237 # pylint: disable=protected-access
2238 endpoints_util.check_list_type(services, remote._ServiceClass, 'services',
2239 allow_none=False)
2240
2241 return self.__api_descriptor(services, hostname=hostname)
2242
2243 def pretty_print_config_to_json(self, services, hostname=None):
2244 """JSON string description of a protorpc.remote.Service in API format.
2245
2246 Args:
2247 services: Either a single protorpc.remote.Service or a list of them
2248 that implements an api/version.
2249 hostname: string, Hostname of the API, to override the value set on the
2250 current service. Defaults to None.
2251
2252 Returns:
2253 string, The API descriptor document as a JSON string.
2254 """
2255 descriptor = self.get_config_dict(services, hostname)
2256 return json.dumps(descriptor, sort_keys=True, indent=2,
2257 separators=(',', ': '))