/circuits/web/client.py
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)