Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1198)

Side by Side Diff: third_party/google-endpoints/endpoints/api_config.py

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

Powered by Google App Engine
This is Rietveld 408576698