1.9.30 sync.
[gae.git] / python / google / appengine / api / request_info.py
blobcb20af48ee76004733751292fd13beda19d3358e
1 #!/usr/bin/env python
3 # Copyright 2007 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 """Allows API stubs to access request and system state when handling calls.
19 Certain API stubs require access to information about the request that triggered
20 the API call (e.g. user_service_stub needs to know the host name of the request
21 to generate continuation URLs) or system state (e.g. modules_stub).
23 Other stubs (e.g. taskqueue_stub, channel_stub) need to be able to dispatch
24 requests within the system.
26 An instance of a RequestInfo subclass is passed to stubs that require these
27 capabilities.
28 """
30 import logging
31 import operator
32 import os
33 import urllib
36 class Error(Exception):
37 pass
40 class ModuleDoesNotExistError(Error):
41 """The provided module does not exist."""
44 class VersionDoesNotExistError(Error):
45 """The provided version does not exist."""
48 class InvalidInstanceIdError(Error):
49 """The provided instance ID is invalid."""
52 class NotSupportedWithAutoScalingError(Error):
53 """The requested operation is not supported for auto-scaling modules."""
56 class VersionAlreadyStartedError(Error):
57 """The version is already started."""
60 class VersionAlreadyStoppedError(Error):
61 """The version is already stopped."""
64 class BackgroundThreadLimitReachedError(Error):
65 """The instance is at its background thread capacity."""
73 class ResponseTuple(tuple):
74 'ResponseTuple(status, headers, content)'
76 __slots__ = ()
78 _fields = ('status', 'headers', 'content')
80 def __new__(cls, status, headers, content):
81 return tuple.__new__(cls, (status, headers, content))
83 @classmethod
84 def _make(cls, iterable, new=tuple.__new__, len=len):
85 result = new(cls, iterable)
86 if len(result) != 3:
87 raise TypeError('Expected 3 arguments, got %d' % len(result))
88 return result
90 def __repr__(self):
91 return 'ResponseTuple(status=%r, headers=%r, content=%r)' % self
93 def _asdict(self):
94 return dict(zip(self._fields, self))
96 __dict__ = property(_asdict)
98 def _replace(self, **kwds):
99 result = self._make(map(kwds.pop, ('status', 'headers', 'content'), self))
100 if kwds:
101 raise ValueError('Got unexpected field names: %r' % kwds.keys())
102 return result
104 def __getnewargs__(self):
105 return tuple(self)
107 status = property(operator.itemgetter(0), doc='Alias for field number 0')
108 headers = property(operator.itemgetter(1), doc='Alias for field number 1')
109 content = property(operator.itemgetter(2), doc='Alias for field number 2')
112 class Dispatcher(object):
113 """Provides information about and dispatches requests to modules."""
115 def get_module_names(self):
116 """Returns a list of module names."""
117 raise NotImplementedError()
119 def get_versions(self, module):
120 """Returns a list of versions for a module.
122 Args:
123 module: A str containing the name of the module.
125 Returns:
126 A list of str containing the versions for the specified module.
128 Raises:
129 ModuleDoesNotExistError: The module does not exist.
131 raise NotImplementedError()
133 def get_default_version(self, module):
134 """Returns the default version for a module.
136 Args:
137 module: A str containing the name of the module.
139 Returns:
140 A str containing the default version for the specified module.
142 Raises:
143 ModuleDoesNotExistError: The module does not exist.
145 raise NotImplementedError()
147 def get_hostname(self, module, version, instance=None):
148 """Returns the hostname for a (module, version, instance) tuple.
150 If instance is set, this will return a hostname for that particular
151 instances. Otherwise, it will return the hostname for load-balancing.
153 Args:
154 module: A str containing the name of the module.
155 version: A str containing the version.
156 instance: An optional str containing the instance ID.
158 Returns:
159 A str containing the hostname.
161 Raises:
162 ModuleDoesNotExistError: The module does not exist.
163 VersionDoesNotExistError: The version does not exist.
164 InvalidInstanceIdError: The instance ID is not valid for the
165 module/version or the module/version uses automatic scaling.
167 raise NotImplementedError()
169 def set_num_instances(self, module, version, instances):
170 """Sets the number of instances to run for a version of a module.
172 Args:
173 module: A str containing the name of the module.
174 version: A str containing the version.
175 instances: An int containing the number of instances to run.
177 Raises:
178 ModuleDoesNotExistError: The module does not exist.
179 VersionDoesNotExistError: The version does not exist.
180 NotSupportedWithAutoScalingError: The provided module/version uses
181 automatic scaling.
183 raise NotImplementedError()
185 def get_num_instances(self, module, version):
186 """Gets the number of instances running for a version of a module.
188 Args:
189 module: A str containing the name of the module.
190 version: A str containing the version.
192 Raises:
193 ModuleDoesNotExistError: The module does not exist.
194 VersionDoesNotExistError: The version does not exist.
195 NotSupportedWithAutoScalingError: The provided module/version uses
196 automatic scaling.
198 raise NotImplementedError()
200 def start_version(self, module, version):
201 """Starts a version.
203 Args:
204 module: A str containing the name of the module.
205 version: A str containing the version.
207 Raises:
208 ModuleDoesNotExistError: The module does not exist.
209 VersionDoesNotExistError: The version does not exist.
210 NotSupportedWithAutoScalingError: The provided module/version uses
211 automatic scaling.
213 raise NotImplementedError()
215 def stop_version(self, module, version):
216 """Stops a version.
218 Args:
219 module: A str containing the name of the module.
220 version: A str containing the version.
222 Raises:
223 ModuleDoesNotExistError: The module does not exist.
224 VersionDoesNotExistError: The version does not exist.
225 NotSupportedWithAutoScalingError: The provided module/version uses
226 automatic scaling.
228 raise NotImplementedError()
230 def add_event(self, runnable, eta, service=None, event_id=None):
231 """Add a callable to be run at the specified time.
233 Args:
234 runnable: A callable object to call at the specified time.
235 eta: An int containing the time to run the event, in seconds since the
236 epoch.
237 service: A str containing the name of the service that owns this event.
238 This should be set if event_id is set.
239 event_id: A str containing the id of the event. If set, this can be passed
240 to update_event to change the time at which the event should run.
242 raise NotImplementedError()
244 def update_event(self, eta, service, event_id):
245 """Update the eta of a scheduled event.
247 Args:
248 eta: An int containing the time to run the event, in seconds since the
249 epoch.
250 service: A str containing the name of the service that owns this event.
251 event_id: A str containing the id of the event to update.
253 raise NotImplementedError()
255 def add_request(self, method, relative_url, headers, body, source_ip,
256 module_name=None, version=None, instance_id=None):
257 """Process an HTTP request.
259 Args:
260 method: A str containing the HTTP method of the request.
261 relative_url: A str containing path and query string of the request.
262 headers: A list of (key, value) tuples where key and value are both str.
263 body: A str containing the request body.
264 source_ip: The source ip address for the request.
265 module_name: An optional str containing the module name to service this
266 request. If unset, the request will be dispatched to the default
267 module.
268 version: An optional str containing the version to service this request.
269 If unset, the request will be dispatched to the default version.
270 instance_id: An optional str containing the instance_id of the instance to
271 service this request. If unset, the request will be dispatched to
272 according to the load-balancing for the module and version.
274 Returns:
275 A ResponseTuple containing the response information for the HTTP request.
277 raise NotImplementedError()
279 def add_async_request(self, method, relative_url, headers, body, source_ip,
280 module_name=None, version=None, instance_id=None):
281 """Dispatch an HTTP request asynchronously.
283 Args:
284 method: A str containing the HTTP method of the request.
285 relative_url: A str containing path and query string of the request.
286 headers: A list of (key, value) tuples where key and value are both str.
287 body: A str containing the request body.
288 source_ip: The source ip address for the request.
289 module_name: An optional str containing the module name to service this
290 request. If unset, the request will be dispatched to the default
291 module.
292 version: An optional str containing the version to service this request.
293 If unset, the request will be dispatched to the default version.
294 instance_id: An optional str containing the instance_id of the instance to
295 service this request. If unset, the request will be dispatched to
296 according to the load-balancing for the module and version.
298 raise NotImplementedError()
300 def send_background_request(self, module_name, version, instance,
301 background_request_id):
302 """Dispatch a background thread request.
304 Args:
305 module_name: A str containing the module name to service this
306 request.
307 version: A str containing the version to service this request.
308 instance: The instance to service this request.
309 background_request_id: A str containing the unique background thread
310 request identifier.
312 Raises:
313 NotSupportedWithAutoScalingError: The provided module/version uses
314 automatic scaling.
315 BackgroundThreadLimitReachedError: The instance is at its background
316 thread capacity.
318 raise NotImplementedError()
323 class _LocalFakeDispatcher(Dispatcher):
324 """A fake Dispatcher implementation usable by tests."""
326 def __init__(self,
327 module_names=None,
328 module_name_to_versions=None,
329 module_name_to_default_versions=None,
330 module_name_to_version_to_hostname=None):
331 super(_LocalFakeDispatcher, self).__init__()
332 if module_names is None:
333 module_names = ['default']
334 if module_name_to_versions is None:
335 module_name_to_versions = {'default': ['1']}
336 if module_name_to_default_versions is None:
337 module_name_to_default_versions = {'default': '1'}
338 if module_name_to_version_to_hostname is None:
339 module_name_to_version_to_hostname = {'default': {'1': 'localhost:8080'}}
340 self._module_names = module_names
341 self._module_name_to_versions = module_name_to_versions
342 self._module_name_to_default_versions = module_name_to_default_versions
343 self._module_name_to_version_to_hostname = (
344 module_name_to_version_to_hostname)
346 def get_module_names(self):
347 """Returns a list of module names."""
348 return self._module_names
350 def get_versions(self, module):
351 """Returns a list of versions for a module.
353 Args:
354 module: A str containing the name of the module.
356 Returns:
357 A list of str containing the versions for the specified module.
359 Raises:
360 ModuleDoesNotExistError: The module does not exist.
362 if module not in self._module_name_to_versions:
363 raise ModuleDoesNotExistError()
364 return self._module_name_to_versions[module]
366 def get_default_version(self, module):
367 """Returns the default version for a module.
369 Args:
370 module: A str containing the name of the module.
372 Returns:
373 A str containing the default version for the specified module.
375 Raises:
376 ModuleDoesNotExistError: The module does not exist.
378 if module not in self._module_name_to_default_versions:
379 raise ModuleDoesNotExistError()
380 return self._module_name_to_default_versions[module]
382 def get_hostname(self, module, version, instance=None):
383 """Returns the hostname for a (module, version, instance) tuple.
385 If instance is set, this will return a hostname for that particular
386 instances. Otherwise, it will return the hostname for load-balancing.
388 Args:
389 module: A str containing the name of the module.
390 version: A str containing the version.
391 instance: An optional str containing the instance ID.
393 Returns:
394 A str containing the hostname.
396 Raises:
397 ModuleDoesNotExistError: The module does not exist.
398 VersionDoesNotExistError: The version does not exist.
399 InvalidInstanceIdError: The instance ID is not valid for the
400 module/version or the module/version uses automatic scaling.
402 if module not in self._module_name_to_version_to_hostname:
403 raise ModuleDoesNotExistError()
404 if version not in self._module_name_to_version_to_hostname[module]:
405 raise VersionDoesNotExistError()
406 if instance:
408 raise InvalidInstanceIdError()
409 return self._module_name_to_version_to_hostname[module][version]
411 def set_num_instances(self, module, version, instances):
412 """Sets the number of instances to run for a version of a module.
414 Args:
415 module: A str containing the name of the module.
416 version: A str containing the version.
417 instances: An int containing the number of instances to run.
419 Raises:
420 ModuleDoesNotExistError: The module does not exist.
421 VersionDoesNotExistError: The version does not exist.
422 NotSupportedWithAutoScalingError: The provided module/version uses
423 automatic scaling.
425 if module not in self._module_name_to_versions:
426 raise ModuleDoesNotExistError()
427 if version not in self._module_name_to_versions[module]:
428 raise VersionDoesNotExistError()
430 raise NotSupportedWithAutoScalingError()
432 def get_num_instances(self, module, version):
433 """Gets the number of instances running for a version of a module.
435 Args:
436 module: A str containing the name of the module.
437 version: A str containing the version.
439 Raises:
440 ModuleDoesNotExistError: The module does not exist.
441 VersionDoesNotExistError: The version does not exist.
442 NotSupportedWithAutoScalingError: The provided module/version uses
443 automatic scaling.
445 if module not in self._module_name_to_versions:
446 raise ModuleDoesNotExistError()
447 if version not in self._module_name_to_versions[module]:
448 raise VersionDoesNotExistError()
450 raise NotSupportedWithAutoScalingError()
452 def start_version(self, module, version):
453 """Starts a version.
455 Args:
456 module: A str containing the name of the module.
457 version: A str containing the version.
459 Raises:
460 ModuleDoesNotExistError: The module does not exist.
461 VersionDoesNotExistError: The version does not exist.
462 NotSupportedWithAutoScalingError: The provided module/version uses
463 automatic scaling.
465 if module not in self._module_name_to_versions:
466 raise ModuleDoesNotExistError()
467 if version not in self._module_name_to_versions[module]:
468 raise VersionDoesNotExistError()
470 raise NotSupportedWithAutoScalingError()
472 def stop_version(self, module, version):
473 """Stops a version.
475 Args:
476 module: A str containing the name of the module.
477 version: A str containing the version.
479 Raises:
480 ModuleDoesNotExistError: The module does not exist.
481 VersionDoesNotExistError: The version does not exist.
482 NotSupportedWithAutoScalingError: The provided module/version uses
483 automatic scaling.
485 if module not in self._module_name_to_versions:
486 raise ModuleDoesNotExistError()
487 if version not in self._module_name_to_versions[module]:
488 raise VersionDoesNotExistError()
490 raise NotSupportedWithAutoScalingError()
492 def add_event(self, runnable, eta, service=None, event_id=None):
493 """Add a callable to be run at the specified time.
495 Args:
496 runnable: A callable object to call at the specified time.
497 eta: An int containing the time to run the event, in seconds since the
498 epoch.
499 service: A str containing the name of the service that owns this event.
500 This should be set if event_id is set.
501 event_id: A str containing the id of the event. If set, this can be passed
502 to update_event to change the time at which the event should run.
504 logging.warning('Scheduled events are not supported with '
505 '_LocalFakeDispatcher')
507 def update_event(self, eta, service, event_id):
508 """Update the eta of a scheduled event.
510 Args:
511 eta: An int containing the time to run the event, in seconds since the
512 epoch.
513 service: A str containing the name of the service that owns this event.
514 event_id: A str containing the id of the event to update.
516 logging.warning('Scheduled events are not supported with '
517 '_LocalFakeDispatcher')
519 def add_request(self, method, relative_url, headers, body, source_ip,
520 module_name=None, version=None, instance_id=None):
521 """Process an HTTP request.
523 Args:
524 method: A str containing the HTTP method of the request.
525 relative_url: A str containing path and query string of the request.
526 headers: A list of (key, value) tuples where key and value are both str.
527 body: A str containing the request body.
528 source_ip: The source ip address for the request.
529 module_name: An optional str containing the module name to service this
530 request. If unset, the request will be dispatched to the default
531 module.
532 version: An optional str containing the version to service this request.
533 If unset, the request will be dispatched to the default version.
534 instance_id: An optional str containing the instance_id of the instance to
535 service this request. If unset, the request will be dispatched to
536 according to the load-balancing for the module and version.
538 Returns:
539 A ResponseTuple containing the response information for the HTTP request.
541 logging.warning('Request dispatching is not supported with '
542 '_LocalFakeDispatcher')
543 return ResponseTuple('501 Not Implemented', [], '')
545 def add_async_request(self, method, relative_url, headers, body, source_ip,
546 module_name=None, version=None, instance_id=None):
547 """Dispatch an HTTP request asynchronously.
549 Args:
550 method: A str containing the HTTP method of the request.
551 relative_url: A str containing path and query string of the request.
552 headers: A list of (key, value) tuples where key and value are both str.
553 body: A str containing the request body.
554 source_ip: The source ip address for the request.
555 module_name: An optional str containing the module name to service this
556 request. If unset, the request will be dispatched to the default
557 module.
558 version: An optional str containing the version to service this request.
559 If unset, the request will be dispatched to the default version.
560 instance_id: An optional str containing the instance_id of the instance to
561 service this request. If unset, the request will be dispatched to
562 according to the load-balancing for the module and version.
564 logging.warning('Request dispatching is not supported with '
565 '_LocalFakeDispatcher')
567 def send_background_request(self, module_name, version, instance,
568 background_request_id):
569 """Dispatch a background thread request.
571 Args:
572 module_name: A str containing the module name to service this
573 request.
574 version: A str containing the version to service this request.
575 instance: The instance to service this request.
576 background_request_id: A str containing the unique background thread
577 request identifier.
579 Raises:
580 NotSupportedWithAutoScalingError: The provided module/version uses
581 automatic scaling.
582 BackgroundThreadLimitReachedError: The instance is at its background
583 thread capacity.
585 logging.warning('Request dispatching is not supported with '
586 '_LocalFakeDispatcher')
587 raise BackgroundThreadLimitReachedError()
589 _local_dispatcher = _LocalFakeDispatcher()
592 class RequestInfo(object):
593 """Allows stubs to lookup state linked to the request making the API call."""
595 def get_request_url(self, request_id):
596 """Returns the URL the request e.g. 'http://localhost:8080/foo?bar=baz'.
598 Args:
599 request_id: The string id of the request making the API call.
601 Returns:
602 The URL of the request as a string.
604 raise NotImplementedError()
606 def get_request_environ(self, request_id):
607 """Returns a dict containing the WSGI environ for the request."""
608 raise NotImplementedError()
610 def get_module(self, request_id):
611 """Returns the name of the module serving this request.
613 Args:
614 request_id: The string id of the request making the API call.
616 Returns:
617 A str containing the module name.
619 raise NotImplementedError()
621 def get_version(self, request_id):
622 """Returns the version of the module serving this request.
624 Args:
625 request_id: The string id of the request making the API call.
627 Returns:
628 A str containing the version.
630 raise NotImplementedError()
632 def get_instance(self, request_id):
633 """Returns the instance serving this request.
635 Args:
636 request_id: The string id of the request making the API call.
638 Returns:
639 An opaque representation of the instance serving this request. It should
640 only be passed to dispatcher methods expecting an instance.
642 raise NotImplementedError()
644 def get_dispatcher(self):
645 """Returns the Dispatcher.
647 Returns:
648 The Dispatcher instance.
650 raise NotImplementedError()
653 class _LocalRequestInfo(RequestInfo):
654 """Lookup information about a request using environment variables."""
656 def get_request_url(self, request_id):
657 """Returns the URL the request e.g. 'http://localhost:8080/foo?bar=baz'.
659 Args:
660 request_id: The string id of the request making the API call.
662 Returns:
663 The URL of the request as a string.
665 try:
666 host = os.environ['HTTP_HOST']
667 except KeyError:
668 host = os.environ['SERVER_NAME']
669 port = os.environ['SERVER_PORT']
670 if port != '80':
671 host += ':' + port
672 url = 'http://' + host
673 url += urllib.quote(os.environ.get('PATH_INFO', '/'))
674 if os.environ.get('QUERY_STRING'):
675 url += '?' + os.environ['QUERY_STRING']
676 return url
678 def get_request_environ(self, request_id):
679 """Returns a dict containing the WSGI environ for the request."""
680 return os.environ
682 def get_module(self, request_id):
683 """Returns the name of the module serving this request.
685 Args:
686 request_id: The string id of the request making the API call.
688 Returns:
689 A str containing the module name.
691 return 'default'
693 def get_version(self, request_id):
694 """Returns the version of the module serving this request.
696 Args:
697 request_id: The string id of the request making the API call.
699 Returns:
700 A str containing the version.
702 return '1'
704 def get_instance(self, request_id):
705 """Returns the instance serving this request.
707 Args:
708 request_id: The string id of the request making the API call.
710 Returns:
711 An opaque representation of the instance serving this request. It should
712 only be passed to dispatcher methods expecting an instance.
714 return object()
716 def get_dispatcher(self):
717 """Returns the Dispatcher.
719 Returns:
720 The Dispatcher instance.
722 return _local_dispatcher
725 _local_request_info = _LocalRequestInfo()