http.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. # -*- coding: utf-8 -*-
  2. """
  3. celery.task.http
  4. ~~~~~~~~~~~~~~~~
  5. Task webhooks implementation.
  6. :copyright: (c) 2009 - 2012 by Ask Solem.
  7. :license: BSD, see LICENSE for more details.
  8. """
  9. from __future__ import absolute_import
  10. import sys
  11. import urllib2
  12. from urllib import urlencode
  13. from urlparse import urlparse
  14. try:
  15. from urlparse import parse_qsl
  16. except ImportError: # pragma: no cover
  17. from cgi import parse_qsl # noqa
  18. from anyjson import deserialize
  19. from .. import __version__ as celery_version
  20. from .base import Task as BaseTask
  21. GET_METHODS = frozenset(["GET", "HEAD"])
  22. class InvalidResponseError(Exception):
  23. """The remote server gave an invalid response."""
  24. class RemoteExecuteError(Exception):
  25. """The remote task gave a custom error."""
  26. class UnknownStatusError(InvalidResponseError):
  27. """The remote server gave an unknown status."""
  28. def maybe_utf8(value):
  29. """Encode to utf-8, only if the value is Unicode."""
  30. if isinstance(value, unicode):
  31. return value.encode("utf-8")
  32. return value
  33. if sys.version_info >= (3, 0):
  34. def utf8dict(tup):
  35. if not isinstance(tup, dict):
  36. return dict(tup)
  37. return tup
  38. else:
  39. def utf8dict(tup): # noqa
  40. """With a dict's items() tuple return a new dict with any utf-8
  41. keys/values encoded."""
  42. return dict((key.encode("utf-8"), maybe_utf8(value))
  43. for key, value in tup)
  44. def extract_response(raw_response):
  45. """Extract the response text from a raw JSON response."""
  46. if not raw_response:
  47. raise InvalidResponseError("Empty response")
  48. try:
  49. payload = deserialize(raw_response)
  50. except ValueError, exc:
  51. raise InvalidResponseError, InvalidResponseError(
  52. str(exc)), sys.exc_info()[2]
  53. status = payload["status"]
  54. if status == "success":
  55. return payload["retval"]
  56. elif status == "failure":
  57. raise RemoteExecuteError(payload.get("reason"))
  58. else:
  59. raise UnknownStatusError(str(status))
  60. class MutableURL(object):
  61. """Object wrapping a Uniform Resource Locator.
  62. Supports editing the query parameter list.
  63. You can convert the object back to a string, the query will be
  64. properly urlencoded.
  65. Examples
  66. >>> url = URL("http://www.google.com:6580/foo/bar?x=3&y=4#foo")
  67. >>> url.query
  68. {'x': '3', 'y': '4'}
  69. >>> str(url)
  70. 'http://www.google.com:6580/foo/bar?y=4&x=3#foo'
  71. >>> url.query["x"] = 10
  72. >>> url.query.update({"George": "Costanza"})
  73. >>> str(url)
  74. 'http://www.google.com:6580/foo/bar?y=4&x=10&George=Costanza#foo'
  75. """
  76. def __init__(self, url):
  77. self.parts = urlparse(url)
  78. self.query = dict(parse_qsl(self.parts[4]))
  79. def __str__(self):
  80. scheme, netloc, path, params, query, fragment = self.parts
  81. query = urlencode(utf8dict(self.query.items()))
  82. components = [scheme + "://", netloc, path or "/",
  83. ";%s" % params if params else "",
  84. "?%s" % query if query else "",
  85. "#%s" % fragment if fragment else ""]
  86. return "".join(filter(None, components))
  87. def __repr__(self):
  88. return "<%s: %s>" % (self.__class__.__name__, str(self))
  89. class HttpDispatch(object):
  90. """Make task HTTP request and collect the task result.
  91. :param url: The URL to request.
  92. :param method: HTTP method used. Currently supported methods are `GET`
  93. and `POST`.
  94. :param task_kwargs: Task keyword arguments.
  95. :param logger: Logger used for user/system feedback.
  96. """
  97. user_agent = "celery/%s" % celery_version
  98. timeout = 5
  99. def __init__(self, url, method, task_kwargs, logger):
  100. self.url = url
  101. self.method = method
  102. self.task_kwargs = task_kwargs
  103. self.logger = logger
  104. def make_request(self, url, method, params):
  105. """Makes an HTTP request and returns the response."""
  106. request = urllib2.Request(url, params)
  107. for key, val in self.http_headers.items():
  108. request.add_header(key, val)
  109. response = urllib2.urlopen(request) # user catches errors.
  110. return response.read()
  111. def dispatch(self):
  112. """Dispatch callback and return result."""
  113. url = MutableURL(self.url)
  114. params = None
  115. if self.method in GET_METHODS:
  116. url.query.update(self.task_kwargs)
  117. else:
  118. params = urlencode(utf8dict(self.task_kwargs.items()))
  119. raw_response = self.make_request(str(url), self.method, params)
  120. return extract_response(raw_response)
  121. @property
  122. def http_headers(self):
  123. headers = {"User-Agent": self.user_agent}
  124. return headers
  125. class HttpDispatchTask(BaseTask):
  126. """Task dispatching to an URL.
  127. :keyword url: The URL location of the HTTP callback task.
  128. :keyword method: Method to use when dispatching the callback. Usually
  129. `GET` or `POST`.
  130. :keyword \*\*kwargs: Keyword arguments to pass on to the HTTP callback.
  131. .. attribute:: url
  132. If this is set, this is used as the default URL for requests.
  133. Default is to require the user of the task to supply the url as an
  134. argument, as this attribute is intended for subclasses.
  135. .. attribute:: method
  136. If this is set, this is the default method used for requests.
  137. Default is to require the user of the task to supply the method as an
  138. argument, as this attribute is intended for subclasses.
  139. """
  140. url = None
  141. method = None
  142. def run(self, url=None, method="GET", **kwargs):
  143. url = url or self.url
  144. method = method or self.method
  145. logger = self.get_logger(**kwargs)
  146. return HttpDispatch(url, method, kwargs, logger).dispatch()
  147. class URL(MutableURL):
  148. """HTTP Callback URL
  149. Supports requesting an URL asynchronously.
  150. :param url: URL to request.
  151. :keyword dispatcher: Class used to dispatch the request.
  152. By default this is :class:`HttpDispatchTask`.
  153. """
  154. dispatcher = HttpDispatchTask
  155. def __init__(self, url, dispatcher=None):
  156. super(URL, self).__init__(url)
  157. self.dispatcher = dispatcher or self.dispatcher
  158. def get_async(self, **kwargs):
  159. return self.dispatcher.delay(str(self), "GET", **kwargs)
  160. def post_async(self, **kwargs):
  161. return self.dispatcher.delay(str(self), "POST", **kwargs)