PageRenderTime 31ms CodeModel.GetById 19ms app.highlight 10ms RepoModel.GetById 1ms app.codeStats 0ms

/circuits/web/client.py

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