/circuits/web/client.py

https://bitbucket.org/prologic/circuits/ · Python · 135 lines · 87 code · 37 blank · 11 comment · 23 complexity · 349c0498cf4980a125134f71e3d688e7 MD5 · raw file

  1. try:
  2. from urllib.parse import urlparse
  3. except ImportError:
  4. from urlparse import urlparse # NOQA
  5. from circuits.protocols.http import HTTP
  6. from circuits.web.headers import Headers
  7. from circuits.net.sockets import TCPClient
  8. from circuits.net.events import close, connect, write
  9. from circuits.core import handler, BaseComponent, Event
  10. def parse_url(url):
  11. p = urlparse(url)
  12. if p.hostname:
  13. host = p.hostname
  14. else:
  15. raise ValueError("URL must be absolute")
  16. if p.scheme == "http":
  17. secure = False
  18. port = p.port or 80
  19. elif p.scheme == "https":
  20. secure = True
  21. port = p.port or 443
  22. else:
  23. raise ValueError("Invalid URL scheme")
  24. path = p.path or "/"
  25. if p.query:
  26. path += "?" + p.query
  27. return (host, port, path, secure)
  28. class HTTPException(Exception):
  29. pass
  30. class NotConnected(HTTPException):
  31. pass
  32. class request(Event):
  33. """request Event
  34. This Event is used to initiate a new request.
  35. :param method: HTTP Method (PUT, GET, POST, DELETE)
  36. :type method: str
  37. :param url: Request URL
  38. :type url: str
  39. """
  40. def __init__(self, method, path, body=None, headers={}):
  41. "x.__init__(...) initializes x; see x.__class__.__doc__ for signature"
  42. super(request, self).__init__(method, path, body, headers)
  43. class Client(BaseComponent):
  44. channel = "client"
  45. def __init__(self, channel=channel):
  46. super(Client, self).__init__(channel=channel)
  47. self._response = None
  48. self._transport = TCPClient(channel=channel).register(self)
  49. HTTP(channel=channel).register(self._transport)
  50. @handler("write")
  51. def write(self, data):
  52. if self._transport.connected:
  53. self.fire(write(data), self._transport)
  54. @handler("close")
  55. def close(self):
  56. if self._transport.connected:
  57. self.fire(close(), self._transport)
  58. @handler("connect", priority=1)
  59. def connect(self, event, host=None, port=None, secure=None):
  60. if not self._transport.connected:
  61. self.fire(connect(host, port, secure), self._transport)
  62. event.stop()
  63. @handler("request")
  64. def request(self, method, url, body=None, headers={}):
  65. host, port, path, secure = parse_url(url)
  66. if not self._transport.connected:
  67. self.fire(connect(host, port, secure))
  68. yield self.wait("connected", self._transport.channel)
  69. headers = Headers([(k, v) for k, v in headers.items()])
  70. # Clients MUST include Host header in HTTP/1.1 requests (RFC 2616)
  71. if "Host" not in headers:
  72. headers["Host"] = "{0:s}{1:s}".format(
  73. host, "" if port in (80, 443) else ":{0:d}".format(port)
  74. )
  75. if body is not None:
  76. headers["Content-Length"] = len(body)
  77. command = "%s %s HTTP/1.1" % (method, path)
  78. message = "%s\r\n%s" % (command, headers)
  79. self.fire(write(message.encode('utf-8')), self._transport)
  80. if body is not None:
  81. self.fire(write(body), self._transport)
  82. yield (yield self.wait("response"))
  83. @handler("response")
  84. def _on_response(self, response):
  85. self._response = response
  86. if response.headers.get("Connection") == "close":
  87. self.fire(close(), self._transport)
  88. return response
  89. @property
  90. def connected(self):
  91. if hasattr(self, "_transport"):
  92. return self._transport.connected
  93. @property
  94. def response(self):
  95. return getattr(self, "_response", None)