class HttpTunnel(RequestBase): first_line = None data = None decompress = False method = 'CONNECT' def __init__(self, client, req): self.client = client self.key = req self.headers = CIMultiDict(client.DEFAULT_TUNNEL_HEADERS) def __repr__(self): return 'Tunnel %s' % self.url __str__ = __repr__ def encode(self): self.headers['host'] = self.key.netloc self.first_line = 'CONNECT http://%s:%s HTTP/1.1' % self.key.address buffer = [self.first_line.encode('ascii'), b'\r\n'] buffer.extend((('%s: %s\r\n' % (name, value)).encode(CHARSET) for name, value in self.headers.items())) buffer.append(b'\r\n') return b''.join(buffer) def has_header(self, header_name): return header_name in self.headers def get_header(self, header_name, default=None): return self.headers.get(header_name, default) def remove_header(self, header_name): self.headers.pop(header_name, None)
async def forward_request( self, request, # type: aiohttp.web.BaseRequest params=None, # type: CIMultiDict data=None, # type: bytes session=None, # type: aiohttp.ClientSession token=None, # type: str ): # type: (...) -> aiohttp.ClientResponse """Forward the given request to our configured homeserver. Args: request (aiohttp.BaseRequest): The request that should be forwarded. params (CIMultiDict, optional): The query parameters for the request. data (Dict, optional): Data for the request. session (aiohttp.ClientSession, optional): The client session that should be used to forward the request. token (str, optional): The access token that should be used for the request. """ if not session: if not self.default_session: self.default_session = ClientSession() session = self.default_session assert session path = urllib.parse.quote( request.path ) # re-encode path stuff like room aliases method = request.method headers = CIMultiDict(request.headers) headers.pop("Host", None) params = params or CIMultiDict(request.query) if token: if "Authorization" in headers: headers["Authorization"] = f"Bearer {token}" if "access_token" in params: params["access_token"] = token if data: data = data headers.pop("Content-Length", None) else: data = await request.read() return await session.request( method, self.homeserver_url + path, data=data, params=params, headers=headers, proxy=self.proxy, ssl=self.ssl, )
class HttpTunnel(RequestBase): first_line = None data = None decompress = False method = 'CONNECT' def __init__(self, client, req): self.client = client self.key = req self.headers = CIMultiDict(client.DEFAULT_TUNNEL_HEADERS) def __repr__(self): return 'Tunnel %s' % self.url __str__ = __repr__ def encode(self): self.headers['host'] = self.key.netloc self.first_line = 'CONNECT http://%s:%s HTTP/1.1' % self.key.address buffer = [self.first_line.encode('ascii'), b'\r\n'] buffer.extend((('%s: %s\r\n' % (name, value)).encode(CHARSET) for name, value in self.headers.items())) buffer.append(b'\r\n') return b''.join(buffer) def has_header(self, header_name): return header_name in self.headers def get_header(self, header_name, default=None): return self.headers.get(header_name, default) def remove_header(self, header_name): self.headers.pop(header_name, None)
def __init__(self, proxies=None, headers=None, verify=True, cookies=None, store_cookies=True, cert=None, max_redirects=10, decompress=True, version=None, websocket_handler=None, parser=None, trust_env=True, loop=None, client_version=None, timeout=None, stream=False, pool_size=10, frame_parser=None, logger=None, close_connections=False, keep_alive=None): super().__init__( partial(Connection, HttpResponse), loop=loop, keep_alive=keep_alive or cfg_value('http_keep_alive') ) self.logger = logger or LOGGER self.client_version = client_version or self.client_version self.connection_pools = {} self.pool_size = pool_size self.trust_env = trust_env self.timeout = timeout self.store_cookies = store_cookies self.max_redirects = max_redirects self.cookies = cookiejar_from_dict(cookies) self.decompress = decompress self.version = version or self.version # SSL Verification default self.verify = verify # SSL client certificate default, if String, path to ssl client # cert file (.pem). If Tuple, ('cert', 'key') pair self.cert = cert self.stream = stream self.close_connections = close_connections dheaders = CIMultiDict(self.DEFAULT_HTTP_HEADERS) dheaders['user-agent'] = self.client_version # override headers if headers: for name, value in mapping_iterator(headers): if value is None: dheaders.pop(name, None) else: dheaders[name] = value self.headers = dheaders self.proxies = dict(proxies or ()) if not self.proxies and self.trust_env: self.proxies = get_environ_proxies() if 'no' not in self.proxies: self.proxies['no'] = ','.join(self.no_proxy) self.websocket_handler = websocket_handler self.http_parser = parser or http.HttpResponseParser self.frame_parser = frame_parser or websocket.frame_parser # Add hooks self.event('on_headers').bind(handle_cookies) self.event('pre_request').bind(WebSocket()) self.event('post_request').bind(Expect()) self.event('post_request').bind(Redirect()) self._decompressors = dict( gzip=GzipDecompress(), deflate=DeflateDecompress() )
async def sync(self, request): access_token = self.get_access_token(request) if not access_token: return self._missing_token try: client_info = self.client_info[access_token] client = self.panta_clients[client_info.user_id] except KeyError: return self._unknown_token sync_filter = request.query.get("filter", None) timeout = request.query.get("timeout", None) try: sync_filter = json.loads(sync_filter) except (JSONDecodeError, TypeError): pass if isinstance(sync_filter, int): sync_filter = None # TODO edit the sync filter to not filter encrypted messages # TODO do the same with an uploaded filter # room_filter = sync_filter.get("room", None) # if room_filter: # timeline_filter = room_filter.get("timeline", None) # if timeline_filter: # types_filter = timeline_filter.get("types", None) query = CIMultiDict(request.query) query.pop("filter", None) response = await self.forward_request(request, query) if response.status == 200: json_response = await response.json() json_response = client.decrypt_sync_body(json_response) return web.Response( status=response.status, text=json.dumps(json_response) ) else: return web.Response( status=response.status, text=await response.text() )
async def forward_request( self, request, params=None, session=None ): # type: (aiohttp.BaseRequest, aiohttp.ClientSession) -> str """Forward the given request to our configured homeserver. Args: request (aiohttp.BaseRequest): The request that should be forwarded. session (aiohttp.ClientSession): The client session that should be used to forward the request. """ if not session: if not self.default_session: self.default_session = ClientSession() session = self.default_session path = request.path method = request.method headers = CIMultiDict(request.headers) headers.pop("Host", None) params = params or request.query data = await request.text() return await session.request( method, self.homeserver + path, data=data, params=params, headers=headers, proxy=self.proxy, ssl=self.ssl )
def __init__(self, body=None, *, status=200, reason=None, text=None, headers=None, charset=None, json_dump_func=None, ctx=None): if json_dump_func is None: json_dump_func = json_dumps body, status = self._process_body(body, status) if charset is None: charset = 'utf-8' body = json_dump_func(body).encode(charset) if headers is None: headers = CIMultiDict() elif not isinstance(headers, (CIMultiDict, CIMultiDictProxy)): headers = CIMultiDict(headers) if hdrs.CONTENT_TYPE in headers: headers.pop(hdrs.CONTENT_TYPE) super().__init__(body=body, status=status, reason=reason, text=text, headers=headers, content_type='application/json', charset=charset, ctx=ctx)
class WsgiResponse: """A WSGI response. Instances are callable using the standard WSGI call and, importantly, iterable:: response = WsgiResponse(200) A :class:`WsgiResponse` is an iterable over bytes to send back to the requesting client. .. attribute:: status_code Integer indicating the HTTP status, (i.e. 200) .. attribute:: response String indicating the HTTP status (i.e. 'OK') .. attribute:: status String indicating the HTTP status code and response (i.e. '200 OK') .. attribute:: content_type The content type of this response. Can be ``None``. .. attribute:: headers The :class:`.Headers` container for this response. .. attribute:: cookies A python :class:`SimpleCookie` container of cookies included in the request as well as cookies set during the response. """ _iterated = False __wsgi_started__ = False def __init__(self, status_code=200, content=None, response_headers=None, content_type=None, encoding=None, can_store_cookies=True): self.status_code = status_code self.encoding = encoding self.headers = CIMultiDict(response_headers or ()) self.content = content self._cookies = None self._can_store_cookies = can_store_cookies if content_type is not None: self.content_type = content_type @property def started(self): return self.__wsgi_started__ @property def iterated(self): return self._iterated @property def cookies(self): if self._cookies is None: self._cookies = SimpleCookie() return self._cookies @property def content(self): return self._content @content.setter def content(self, content): self.set_content(content) def set_content(self, content): if self._iterated: raise RuntimeError('Cannot set content. Already iterated') if content is None: self._content = () elif isinstance(content, str): if not self.encoding: # use utf-8 if not set self.encoding = 'utf-8' self._content = content.encode(self.encoding), elif isinstance(content, bytes): self._content = content, else: self._content = content def _get_content_type(self): return self.headers.get('content-type') def _set_content_type(self, typ): if typ: self.headers['content-type'] = typ else: self.headers.pop('content-type', None) content_type = property(_get_content_type, _set_content_type) @property def response(self): return responses.get(self.status_code) @property def status(self): return '%s %s' % (self.status_code, responses.get(self.status_code)) def __str__(self): return self.status def __repr__(self): return '%s(%s)' % (self.__class__.__name__, self) def is_streamed(self): """Check if the response is streamed. A streamed response is an iterable with no length information. In this case streamed means that there is no information about the number of iterations. This is usually `True` if a generator is passed to the response object. """ try: len(self._content) except TypeError: return True return False def length(self): try: len(self._content) except TypeError: return return reduce(count_len, self._content, 0) def can_set_cookies(self): return self.status_code < 400 and self._can_store_cookies def start(self, environ, start_response, exc_info=None): self.__wsgi_started__ = True headers = self._get_headers(environ) return start_response(self.status, headers, exc_info) def __iter__(self): if self._iterated: raise RuntimeError('WsgiResponse can be iterated once only') self.__wsgi_started__ = True self._iterated = True iterable = iter(self._content) self._content = None return iterable def close(self): """Close this response, required by WSGI """ if hasattr(self._content, 'close'): self._content.close() def set_cookie(self, key, **kwargs): """ Sets a cookie. ``expires`` can be a string in the correct format or a ``datetime.datetime`` object in UTC. If ``expires`` is a datetime object then ``max_age`` will be calculated. """ set_cookie(self.cookies, key, **kwargs) def delete_cookie(self, key, path='/', domain=None): set_cookie(self.cookies, key, max_age=0, path=path, domain=domain, expires='Thu, 01-Jan-1970 00:00:00 GMT') def has_header(self, header): return header in self.headers __contains__ = has_header def __setitem__(self, header, value): self.headers[header] = value def __getitem__(self, header): return self.headers[header] def _get_headers(self, environ): """The list of headers for this response """ headers = self.headers method = environ['REQUEST_METHOD'] if has_empty_content(self.status_code, method) and method != HEAD: headers.pop('content-type', None) headers.pop('content-length', None) self._content = () else: if not self.is_streamed(): cl = reduce(count_len, self._content, 0) headers['content-length'] = str(cl) ct = headers.get('content-type') # content type encoding available if self.encoding: ct = ct or 'text/plain' if ';' not in ct: ct = '%s; charset=%s' % (ct, self.encoding) if ct: headers['content-type'] = ct if method == HEAD: self._content = () # Cookies if (self.status_code < 400 and self._can_store_cookies and self._cookies): for c in self.cookies.values(): headers.add('set-cookie', c.OutputString()) return headers.items()
class StreamResponse(BaseClass, HeadersMixin): _length_check = True def __init__(self, *, status: int = 200, reason: Optional[str] = None, headers: Optional[LooseHeaders] = None) -> None: self._body = None self._keep_alive = None # type: Optional[bool] self._chunked = False self._compression = False self._compression_force = None # type: Optional[ContentCoding] self._cookies = SimpleCookie() # type: SimpleCookie[str] self._req = None # type: Optional[BaseRequest] self._payload_writer = None # type: Optional[AbstractStreamWriter] self._eof_sent = False self._body_length = 0 self._state = {} # type: Dict[str, Any] if headers is not None: self._headers = CIMultiDict(headers) # type: CIMultiDict[str] else: self._headers = CIMultiDict() self.set_status(status, reason) @property def prepared(self) -> bool: return self._payload_writer is not None @property def task(self) -> "asyncio.Task[None]": return getattr(self._req, "task", None) @property def status(self) -> int: return self._status @property def chunked(self) -> bool: return self._chunked @property def compression(self) -> bool: return self._compression @property def reason(self) -> str: return self._reason def set_status( self, status: int, reason: Optional[str] = None, _RESPONSES: Mapping[int, Tuple[str, str]] = RESPONSES, ) -> None: assert not self.prepared, ( "Cannot change the response status code after " "the headers have been sent") self._status = int(status) if reason is None: try: reason = _RESPONSES[self._status][0] except Exception: reason = "" self._reason = reason @property def keep_alive(self) -> Optional[bool]: return self._keep_alive def force_close(self) -> None: self._keep_alive = False @property def body_length(self) -> int: return self._body_length @property def output_length(self) -> int: warnings.warn("output_length is deprecated", DeprecationWarning) assert self._payload_writer return self._payload_writer.buffer_size def enable_chunked_encoding(self, chunk_size: Optional[int] = None) -> None: """Enables automatic chunked transfer encoding.""" self._chunked = True if hdrs.CONTENT_LENGTH in self._headers: raise RuntimeError("You can't enable chunked encoding when " "a content length is set") if chunk_size is not None: warnings.warn("Chunk size is deprecated #1615", DeprecationWarning) def enable_compression(self, force: Optional[Union[bool, ContentCoding]] = None ) -> None: """Enables response compression encoding.""" # Backwards compatibility for when force was a bool <0.17. if type(force) == bool: force = ContentCoding.deflate if force else ContentCoding.identity warnings.warn("Using boolean for force is deprecated #3318", DeprecationWarning) elif force is not None: assert isinstance(force, ContentCoding), ("force should one of " "None, bool or " "ContentEncoding") self._compression = True self._compression_force = force @property def headers(self) -> "CIMultiDict[str]": return self._headers @property def cookies(self) -> "SimpleCookie[str]": return self._cookies def set_cookie(self, name: str, value: str, *, expires: Optional[str] = None, domain: Optional[str] = None, max_age: Optional[Union[int, str]] = None, path: str = "/", secure: Optional[bool] = None, httponly: Optional[bool] = None, version: Optional[str] = None, samesite: Optional[str] = None) -> None: """Set or update response cookie. Sets new cookie or updates existent with new value. Also updates only those params which are not None. """ old = self._cookies.get(name) if old is not None and old.coded_value == "": # deleted cookie self._cookies.pop(name, None) self._cookies[name] = value c = self._cookies[name] if expires is not None: c["expires"] = expires elif c.get("expires") == "Thu, 01 Jan 1970 00:00:00 GMT": del c["expires"] if domain is not None: c["domain"] = domain if max_age is not None: c["max-age"] = str(max_age) elif "max-age" in c: del c["max-age"] c["path"] = path if secure is not None: c["secure"] = secure if httponly is not None: c["httponly"] = httponly if version is not None: c["version"] = version if samesite is not None: c["samesite"] = samesite def del_cookie(self, name: str, *, domain: Optional[str] = None, path: str = "/") -> None: """Delete cookie. Creates new empty expired cookie. """ # TODO: do we need domain/path here? self._cookies.pop(name, None) self.set_cookie( name, "", max_age=0, expires="Thu, 01 Jan 1970 00:00:00 GMT", domain=domain, path=path, ) @property def content_length(self) -> Optional[int]: # Just a placeholder for adding setter return super().content_length @content_length.setter def content_length(self, value: Optional[int]) -> None: if value is not None: value = int(value) if self._chunked: raise RuntimeError("You can't set content length when " "chunked encoding is enable") self._headers[hdrs.CONTENT_LENGTH] = str(value) else: self._headers.pop(hdrs.CONTENT_LENGTH, None) @property def content_type(self) -> str: # Just a placeholder for adding setter return super().content_type @content_type.setter def content_type(self, value: str) -> None: self.content_type # read header values if needed self._content_type = str(value) self._generate_content_type_header() @property def charset(self) -> Optional[str]: # Just a placeholder for adding setter return super().charset @charset.setter def charset(self, value: Optional[str]) -> None: ctype = self.content_type # read header values if needed if ctype == "application/octet-stream": raise RuntimeError("Setting charset for application/octet-stream " "doesn't make sense, setup content_type first") assert self._content_dict is not None if value is None: self._content_dict.pop("charset", None) else: self._content_dict["charset"] = str(value).lower() self._generate_content_type_header() @property def last_modified(self) -> Optional[datetime.datetime]: """The value of Last-Modified HTTP header, or None. This header is represented as a `datetime` object. """ httpdate = self._headers.get(hdrs.LAST_MODIFIED) if httpdate is not None: timetuple = parsedate(httpdate) if timetuple is not None: return datetime.datetime(*timetuple[:6], tzinfo=datetime.timezone.utc) return None @last_modified.setter def last_modified( self, value: Optional[Union[int, float, datetime.datetime, str]]) -> None: if value is None: self._headers.pop(hdrs.LAST_MODIFIED, None) elif isinstance(value, (int, float)): self._headers[hdrs.LAST_MODIFIED] = time.strftime( "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(math.ceil(value))) elif isinstance(value, datetime.datetime): self._headers[hdrs.LAST_MODIFIED] = time.strftime( "%a, %d %b %Y %H:%M:%S GMT", value.utctimetuple()) elif isinstance(value, str): self._headers[hdrs.LAST_MODIFIED] = value def _generate_content_type_header(self, CONTENT_TYPE: istr = hdrs.CONTENT_TYPE ) -> None: assert self._content_dict is not None assert self._content_type is not None params = "; ".join("{}={}".format(k, v) for k, v in self._content_dict.items()) if params: ctype = self._content_type + "; " + params else: ctype = self._content_type self._headers[CONTENT_TYPE] = ctype async def _do_start_compression(self, coding: ContentCoding) -> None: if coding != ContentCoding.identity: assert self._payload_writer is not None self._headers[hdrs.CONTENT_ENCODING] = coding.value self._payload_writer.enable_compression(coding.value) # Compressed payload may have different content length, # remove the header self._headers.popall(hdrs.CONTENT_LENGTH, None) async def _start_compression(self, request: "BaseRequest") -> None: if self._compression_force: await self._do_start_compression(self._compression_force) else: accept_encoding = request.headers.get(hdrs.ACCEPT_ENCODING, "").lower() for coding in ContentCoding: if coding.value in accept_encoding: await self._do_start_compression(coding) return async def prepare( self, request: "BaseRequest") -> Optional[AbstractStreamWriter]: if self._eof_sent: return None if self._payload_writer is not None: return self._payload_writer return await self._start(request) async def _start(self, request: "BaseRequest") -> AbstractStreamWriter: self._req = request writer = self._payload_writer = request._payload_writer await self._prepare_headers() await request._prepare_hook(self) await self._write_headers() return writer async def _prepare_headers(self) -> None: request = self._req assert request is not None writer = self._payload_writer assert writer is not None keep_alive = self._keep_alive if keep_alive is None: keep_alive = request.keep_alive self._keep_alive = keep_alive version = request.version headers = self._headers for cookie in self._cookies.values(): value = cookie.output(header="")[1:] headers.add(hdrs.SET_COOKIE, value) if self._compression: await self._start_compression(request) if self._chunked: if version != HttpVersion11: raise RuntimeError("Using chunked encoding is forbidden " "for HTTP/{0.major}.{0.minor}".format( request.version)) writer.enable_chunking() headers[hdrs.TRANSFER_ENCODING] = "chunked" if hdrs.CONTENT_LENGTH in headers: del headers[hdrs.CONTENT_LENGTH] elif self._length_check: writer.length = self.content_length if writer.length is None: if version >= HttpVersion11: writer.enable_chunking() headers[hdrs.TRANSFER_ENCODING] = "chunked" if hdrs.CONTENT_LENGTH in headers: del headers[hdrs.CONTENT_LENGTH] else: keep_alive = False # HTTP 1.1: https://tools.ietf.org/html/rfc7230#section-3.3.2 # HTTP 1.0: https://tools.ietf.org/html/rfc1945#section-10.4 elif version >= HttpVersion11 and self.status in (100, 101, 102, 103, 204): del headers[hdrs.CONTENT_LENGTH] headers.setdefault(hdrs.CONTENT_TYPE, "application/octet-stream") headers.setdefault(hdrs.DATE, rfc822_formatted_time()) headers.setdefault(hdrs.SERVER, SERVER_SOFTWARE) # connection header if hdrs.CONNECTION not in headers: if keep_alive: if version == HttpVersion10: headers[hdrs.CONNECTION] = "keep-alive" else: if version == HttpVersion11: headers[hdrs.CONNECTION] = "close" async def _write_headers(self) -> None: request = self._req assert request is not None writer = self._payload_writer assert writer is not None # status line version = request.version status_line = "HTTP/{}.{} {} {}".format(version[0], version[1], self._status, self._reason) await writer.write_headers(status_line, self._headers) async def write(self, data: bytes) -> None: assert isinstance( data, (bytes, bytearray, memoryview)), "data argument must be byte-ish (%r)" % type(data) if self._eof_sent: raise RuntimeError("Cannot call write() after write_eof()") if self._payload_writer is None: raise RuntimeError("Cannot call write() before prepare()") await self._payload_writer.write(data) async def drain(self) -> None: assert not self._eof_sent, "EOF has already been sent" assert self._payload_writer is not None, "Response has not been started" warnings.warn( "drain method is deprecated, use await resp.write()", DeprecationWarning, stacklevel=2, ) await self._payload_writer.drain() async def write_eof(self, data: bytes = b"") -> None: assert isinstance( data, (bytes, bytearray, memoryview)), "data argument must be byte-ish (%r)" % type(data) if self._eof_sent: return assert self._payload_writer is not None, "Response has not been started" await self._payload_writer.write_eof(data) self._eof_sent = True self._req = None self._body_length = self._payload_writer.output_size self._payload_writer = None def __repr__(self) -> str: if self._eof_sent: info = "eof" elif self.prepared: assert self._req is not None info = "{} {} ".format(self._req.method, self._req.path) else: info = "not prepared" return "<{} {} {}>".format(self.__class__.__name__, self.reason, info) def __getitem__(self, key: str) -> Any: return self._state[key] def __setitem__(self, key: str, value: Any) -> None: self._state[key] = value def __delitem__(self, key: str) -> None: del self._state[key] def __len__(self) -> int: return len(self._state) def __iter__(self) -> Iterator[str]: return iter(self._state) def __hash__(self) -> int: return hash(id(self)) def __eq__(self, other: object) -> bool: return self is other
class WsgiResponse: """A WSGI response. Instances are callable using the standard WSGI call and, importantly, iterable:: response = WsgiResponse(200) A :class:`WsgiResponse` is an iterable over bytes to send back to the requesting client. .. attribute:: status_code Integer indicating the HTTP status, (i.e. 200) .. attribute:: response String indicating the HTTP status (i.e. 'OK') .. attribute:: status String indicating the HTTP status code and response (i.e. '200 OK') .. attribute:: content_type The content type of this response. Can be ``None``. .. attribute:: headers The :class:`.Headers` container for this response. .. attribute:: cookies A python :class:`SimpleCookie` container of cookies included in the request as well as cookies set during the response. """ _iterated = False __wsgi_started__ = False def __init__(self, status_code=200, content=None, response_headers=None, content_type=None, encoding=None, can_store_cookies=True): self.status_code = status_code self.encoding = encoding self.headers = CIMultiDict(response_headers or ()) self.content = content self._cookies = None self._can_store_cookies = can_store_cookies if content_type is not None: self.content_type = content_type @property def started(self): return self.__wsgi_started__ @property def iterated(self): return self._iterated @property def cookies(self): if self._cookies is None: self._cookies = SimpleCookie() return self._cookies @property def content(self): return self._content @content.setter def content(self, content): self.set_content(content) def set_content(self, content): if self._iterated: raise RuntimeError('Cannot set content. Already iterated') if content is None: self._content = () elif isinstance(content, str): if not self.encoding: # use utf-8 if not set self.encoding = 'utf-8' self._content = content.encode(self.encoding), elif isinstance(content, bytes): self._content = content, else: self._content = content def _get_content_type(self): return self.headers.get('content-type') def _set_content_type(self, typ): if typ: self.headers['content-type'] = typ else: self.headers.pop('content-type', None) content_type = property(_get_content_type, _set_content_type) @property def response(self): return responses.get(self.status_code) @property def status(self): return '%s %s' % (self.status_code, responses.get(self.status_code)) def __str__(self): return self.status def __repr__(self): return '%s(%s)' % (self.__class__.__name__, self) def is_streamed(self): """Check if the response is streamed. A streamed response is an iterable with no length information. In this case streamed means that there is no information about the number of iterations. This is usually `True` if a generator is passed to the response object. """ try: len(self._content) except TypeError: return True return False def length(self): try: len(self._content) except TypeError: return return reduce(count_len, self._content, 0) def can_set_cookies(self): return self.status_code < 400 and self._can_store_cookies def start(self, environ, start_response, exc_info=None): self.__wsgi_started__ = True headers = self._get_headers(environ) return start_response(self.status, headers, exc_info) def __iter__(self): if self._iterated: raise RuntimeError('WsgiResponse can be iterated once only') self.__wsgi_started__ = True self._iterated = True iterable = iter(self._content) self._content = None return iterable def close(self): """Close this response, required by WSGI """ if hasattr(self._content, 'close'): self._content.close() def set_cookie(self, key, **kwargs): """ Sets a cookie. ``expires`` can be a string in the correct format or a ``datetime.datetime`` object in UTC. If ``expires`` is a datetime object then ``max_age`` will be calculated. """ set_cookie(self.cookies, key, **kwargs) def delete_cookie(self, key, path='/', domain=None): set_cookie(self.cookies, key, max_age=0, path=path, domain=domain, expires='Thu, 01-Jan-1970 00:00:00 GMT') def has_header(self, header): return header in self.headers __contains__ = has_header def __setitem__(self, header, value): self.headers[header] = value def __getitem__(self, header): return self.headers[header] def _get_headers(self, environ): """The list of headers for this response """ headers = self.headers method = environ['REQUEST_METHOD'] if has_empty_content(self.status_code, method) and method != HEAD: headers.pop('content-type', None) headers.pop('content-length', None) self._content = () else: if not self.is_streamed(): cl = reduce(count_len, self._content, 0) headers['content-length'] = str(cl) ct = headers.get('content-type') # content type encoding available if self.encoding: ct = ct or 'text/plain' if ';' not in ct: ct = '%s; charset=%s' % (ct, self.encoding) if ct: headers['content-type'] = ct if method == HEAD: self._content = () # Cookies if (self.status_code < 400 and self._can_store_cookies and self._cookies): for c in self.cookies.values(): headers.add('set-cookie', c.OutputString()) return headers.items()
class StreamResponse(HeadersMixin): _length_check = True def __init__(self, *, status=200, reason=None, headers=None): self._body = None self._keep_alive = None self._chunked = False self._compression = False self._compression_force = False self._cookies = SimpleCookie() self._req = None self._payload_writer = None self._eof_sent = False self._body_length = 0 if headers is not None: self._headers = CIMultiDict(headers) else: self._headers = CIMultiDict() self.set_status(status, reason) @property def prepared(self): return self._payload_writer is not None @property def task(self): return getattr(self._req, 'task', None) @property def status(self): return self._status @property def chunked(self): return self._chunked @property def compression(self): return self._compression @property def reason(self): return self._reason def set_status(self, status, reason=None, _RESPONSES=RESPONSES): assert not self.prepared, \ 'Cannot change the response status code after ' \ 'the headers have been sent' self._status = int(status) if reason is None: try: reason = _RESPONSES[self._status][0] except: reason = '' self._reason = reason @property def keep_alive(self): return self._keep_alive def force_close(self): self._keep_alive = False @property def body_length(self): return self._body_length @property def output_length(self): warnings.warn('output_length is deprecated', DeprecationWarning) return self._payload_writer.buffer_size def enable_chunked_encoding(self, chunk_size=None): """Enables automatic chunked transfer encoding.""" self._chunked = True if chunk_size is not None: warnings.warn('Chunk size is deprecated #1615', DeprecationWarning) def enable_compression(self, force=None): """Enables response compression encoding.""" # Backwards compatibility for when force was a bool <0.17. if type(force) == bool: force = ContentCoding.deflate if force else ContentCoding.identity elif force is not None: assert isinstance(force, ContentCoding), ("force should one of " "None, bool or " "ContentEncoding") self._compression = True self._compression_force = force @property def headers(self): return self._headers @property def cookies(self): return self._cookies def set_cookie(self, name, value, *, expires=None, domain=None, max_age=None, path='/', secure=None, httponly=None, version=None): """Set or update response cookie. Sets new cookie or updates existent with new value. Also updates only those params which are not None. """ old = self._cookies.get(name) if old is not None and old.coded_value == '': # deleted cookie self._cookies.pop(name, None) self._cookies[name] = value c = self._cookies[name] if expires is not None: c['expires'] = expires elif c.get('expires') == 'Thu, 01 Jan 1970 00:00:00 GMT': del c['expires'] if domain is not None: c['domain'] = domain if max_age is not None: c['max-age'] = max_age elif 'max-age' in c: del c['max-age'] c['path'] = path if secure is not None: c['secure'] = secure if httponly is not None: c['httponly'] = httponly if version is not None: c['version'] = version def del_cookie(self, name, *, domain=None, path='/'): """Delete cookie. Creates new empty expired cookie. """ # TODO: do we need domain/path here? self._cookies.pop(name, None) self.set_cookie(name, '', max_age=0, expires="Thu, 01 Jan 1970 00:00:00 GMT", domain=domain, path=path) @property def content_length(self): # Just a placeholder for adding setter return super().content_length @content_length.setter def content_length(self, value): if value is not None: value = int(value) # TODO: raise error if chunked enabled self._headers[hdrs.CONTENT_LENGTH] = str(value) else: self._headers.pop(hdrs.CONTENT_LENGTH, None) @property def content_type(self): # Just a placeholder for adding setter return super().content_type @content_type.setter def content_type(self, value): self.content_type # read header values if needed self._content_type = str(value) self._generate_content_type_header() @property def charset(self): # Just a placeholder for adding setter return super().charset @charset.setter def charset(self, value): ctype = self.content_type # read header values if needed if ctype == 'application/octet-stream': raise RuntimeError("Setting charset for application/octet-stream " "doesn't make sense, setup content_type first") if value is None: self._content_dict.pop('charset', None) else: self._content_dict['charset'] = str(value).lower() self._generate_content_type_header() @property def last_modified(self, _LAST_MODIFIED=hdrs.LAST_MODIFIED): """The value of Last-Modified HTTP header, or None. This header is represented as a `datetime` object. """ httpdate = self.headers.get(_LAST_MODIFIED) if httpdate is not None: timetuple = parsedate(httpdate) if timetuple is not None: return datetime.datetime(*timetuple[:6], tzinfo=datetime.timezone.utc) return None @last_modified.setter def last_modified(self, value): if value is None: self.headers.pop(hdrs.LAST_MODIFIED, None) elif isinstance(value, (int, float)): self.headers[hdrs.LAST_MODIFIED] = time.strftime( "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(math.ceil(value))) elif isinstance(value, datetime.datetime): self.headers[hdrs.LAST_MODIFIED] = time.strftime( "%a, %d %b %Y %H:%M:%S GMT", value.utctimetuple()) elif isinstance(value, str): self.headers[hdrs.LAST_MODIFIED] = value @property def tcp_nodelay(self): payload_writer = self._payload_writer assert payload_writer is not None, \ "Cannot get tcp_nodelay for not prepared response" return payload_writer.tcp_nodelay def set_tcp_nodelay(self, value): payload_writer = self._payload_writer assert payload_writer is not None, \ "Cannot set tcp_nodelay for not prepared response" payload_writer.set_tcp_nodelay(value) @property def tcp_cork(self): payload_writer = self._payload_writer assert payload_writer is not None, \ "Cannot get tcp_cork for not prepared response" return payload_writer.tcp_cork def set_tcp_cork(self, value): payload_writer = self._payload_writer assert payload_writer is not None, \ "Cannot set tcp_cork for not prepared response" payload_writer.set_tcp_cork(value) def _generate_content_type_header(self, CONTENT_TYPE=hdrs.CONTENT_TYPE): params = '; '.join("%s=%s" % i for i in self._content_dict.items()) if params: ctype = self._content_type + '; ' + params else: ctype = self._content_type self.headers[CONTENT_TYPE] = ctype def _do_start_compression(self, coding): if coding != ContentCoding.identity: self.headers[hdrs.CONTENT_ENCODING] = coding.value self._payload_writer.enable_compression(coding.value) self._chunked = True def _start_compression(self, request): if self._compression_force: self._do_start_compression(self._compression_force) else: accept_encoding = request.headers.get(hdrs.ACCEPT_ENCODING, '').lower() for coding in ContentCoding: if coding.value in accept_encoding: self._do_start_compression(coding) return @asyncio.coroutine def prepare(self, request): if self._eof_sent: return if self._payload_writer is not None: return self._payload_writer yield from request._prepare_hook(self) return self._start(request) def _start(self, request, HttpVersion10=HttpVersion10, HttpVersion11=HttpVersion11, CONNECTION=hdrs.CONNECTION, DATE=hdrs.DATE, SERVER=hdrs.SERVER, CONTENT_TYPE=hdrs.CONTENT_TYPE, CONTENT_LENGTH=hdrs.CONTENT_LENGTH, SET_COOKIE=hdrs.SET_COOKIE, SERVER_SOFTWARE=SERVER_SOFTWARE, TRANSFER_ENCODING=hdrs.TRANSFER_ENCODING): self._req = request keep_alive = self._keep_alive if keep_alive is None: keep_alive = request.keep_alive self._keep_alive = keep_alive version = request.version writer = self._payload_writer = request._writer headers = self._headers for cookie in self._cookies.values(): value = cookie.output(header='')[1:] headers.add(SET_COOKIE, value) if self._compression: self._start_compression(request) if self._chunked: if version != HttpVersion11: raise RuntimeError("Using chunked encoding is forbidden " "for HTTP/{0.major}.{0.minor}".format( request.version)) writer.enable_chunking() headers[TRANSFER_ENCODING] = 'chunked' if CONTENT_LENGTH in headers: del headers[CONTENT_LENGTH] elif self._length_check: writer.length = self.content_length if writer.length is None and version >= HttpVersion11: writer.enable_chunking() headers[TRANSFER_ENCODING] = 'chunked' if CONTENT_LENGTH in headers: del headers[CONTENT_LENGTH] headers.setdefault(CONTENT_TYPE, 'application/octet-stream') headers.setdefault(DATE, request.time_service.strtime()) headers.setdefault(SERVER, SERVER_SOFTWARE) # connection header if CONNECTION not in headers: if keep_alive: if version == HttpVersion10: headers[CONNECTION] = 'keep-alive' else: if version == HttpVersion11: headers[CONNECTION] = 'close' # status line status_line = 'HTTP/{}.{} {} {}\r\n'.format(version[0], version[1], self._status, self._reason) writer.write_headers(status_line, headers) return writer def write(self, data): assert isinstance(data, (bytes, bytearray, memoryview)), \ "data argument must be byte-ish (%r)" % type(data) if self._eof_sent: raise RuntimeError("Cannot call write() after write_eof()") if self._payload_writer is None: raise RuntimeError("Cannot call write() before start()") return self._payload_writer.write(data) @asyncio.coroutine def drain(self): assert not self._eof_sent, "EOF has already been sent" assert self._payload_writer is not None, \ "Response has not been started" yield from self._payload_writer.drain() @asyncio.coroutine def write_eof(self, data=b''): assert isinstance(data, (bytes, bytearray, memoryview)), \ "data argument must be byte-ish (%r)" % type(data) if self._eof_sent: return assert self._payload_writer is not None, \ "Response has not been started" yield from self._payload_writer.write_eof(data) self._eof_sent = True self._req = None self._body_length = self._payload_writer.output_size self._payload_writer = None def __repr__(self): if self._eof_sent: info = "eof" elif self.prepared: info = "{} {} ".format(self._req.method, self._req.path) else: info = "not prepared" return "<{} {} {}>".format(self.__class__.__name__, self.reason, info)
class StreamResponse(BaseClass, HeadersMixin, CookieMixin): __slots__ = ( "_length_check", "_body", "_keep_alive", "_chunked", "_compression", "_compression_force", "_req", "_payload_writer", "_eof_sent", "_body_length", "_state", "_headers", "_status", "_reason", "__weakref__", ) def __init__( self, *, status: int = 200, reason: Optional[str] = None, headers: Optional[LooseHeaders] = None, ) -> None: super().__init__() self._length_check = True self._body = None self._keep_alive: Optional[bool] = None self._chunked = False self._compression = False self._compression_force: Optional[ContentCoding] = None self._req: Optional[BaseRequest] = None self._payload_writer: Optional[AbstractStreamWriter] = None self._eof_sent = False self._body_length = 0 self._state: Dict[str, Any] = {} if headers is not None: self._headers: CIMultiDict[str] = CIMultiDict(headers) else: self._headers = CIMultiDict() self.set_status(status, reason) @property def prepared(self) -> bool: return self._payload_writer is not None @property def task(self) -> "Optional[asyncio.Task[None]]": if self._req: return self._req.task else: return None @property def status(self) -> int: return self._status @property def chunked(self) -> bool: return self._chunked @property def compression(self) -> bool: return self._compression @property def reason(self) -> str: return self._reason def set_status( self, status: int, reason: Optional[str] = None, _RESPONSES: Mapping[int, Tuple[str, str]] = RESPONSES, ) -> None: assert not self.prepared, ( "Cannot change the response status code after " "the headers have been sent") self._status = int(status) if reason is None: try: reason = _RESPONSES[self._status][0] except Exception: reason = "" self._reason = reason @property def keep_alive(self) -> Optional[bool]: return self._keep_alive def force_close(self) -> None: self._keep_alive = False @property def body_length(self) -> int: return self._body_length def enable_chunked_encoding(self) -> None: """Enables automatic chunked transfer encoding.""" self._chunked = True if hdrs.CONTENT_LENGTH in self._headers: raise RuntimeError("You can't enable chunked encoding when " "a content length is set") def enable_compression(self, force: Optional[ContentCoding] = None) -> None: """Enables response compression encoding.""" # Backwards compatibility for when force was a bool <0.17. self._compression = True self._compression_force = force @property def headers(self) -> "CIMultiDict[str]": return self._headers @property def content_length(self) -> Optional[int]: # Just a placeholder for adding setter return super().content_length @content_length.setter def content_length(self, value: Optional[int]) -> None: if value is not None: value = int(value) if self._chunked: raise RuntimeError("You can't set content length when " "chunked encoding is enable") self._headers[hdrs.CONTENT_LENGTH] = str(value) else: self._headers.pop(hdrs.CONTENT_LENGTH, None) @property def content_type(self) -> str: # Just a placeholder for adding setter return super().content_type @content_type.setter def content_type(self, value: str) -> None: self.content_type # read header values if needed self._content_type = str(value) self._generate_content_type_header() @property def charset(self) -> Optional[str]: # Just a placeholder for adding setter return super().charset @charset.setter def charset(self, value: Optional[str]) -> None: ctype = self.content_type # read header values if needed if ctype == "application/octet-stream": raise RuntimeError("Setting charset for application/octet-stream " "doesn't make sense, setup content_type first") assert self._content_dict is not None if value is None: self._content_dict.pop("charset", None) else: self._content_dict["charset"] = str(value).lower() self._generate_content_type_header() @property def last_modified(self) -> Optional[datetime.datetime]: """The value of Last-Modified HTTP header, or None. This header is represented as a `datetime` object. """ return parse_http_date(self._headers.get(hdrs.LAST_MODIFIED)) @last_modified.setter def last_modified( self, value: Optional[Union[int, float, datetime.datetime, str]]) -> None: if value is None: self._headers.pop(hdrs.LAST_MODIFIED, None) elif isinstance(value, (int, float)): self._headers[hdrs.LAST_MODIFIED] = time.strftime( "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(math.ceil(value))) elif isinstance(value, datetime.datetime): self._headers[hdrs.LAST_MODIFIED] = time.strftime( "%a, %d %b %Y %H:%M:%S GMT", value.utctimetuple()) elif isinstance(value, str): self._headers[hdrs.LAST_MODIFIED] = value @property def etag(self) -> Optional[ETag]: quoted_value = self._headers.get(hdrs.ETAG) if not quoted_value: return None elif quoted_value == ETAG_ANY: return ETag(value=ETAG_ANY) match = QUOTED_ETAG_RE.fullmatch(quoted_value) if not match: return None is_weak, value = match.group(1, 2) return ETag( is_weak=bool(is_weak), value=value, ) @etag.setter def etag(self, value: Optional[Union[ETag, str]]) -> None: if value is None: self._headers.pop(hdrs.ETAG, None) elif (isinstance(value, str) and value == ETAG_ANY) or (isinstance(value, ETag) and value.value == ETAG_ANY): self._headers[hdrs.ETAG] = ETAG_ANY elif isinstance(value, str): validate_etag_value(value) self._headers[hdrs.ETAG] = f'"{value}"' elif isinstance(value, ETag) and isinstance(value.value, str): validate_etag_value(value.value) hdr_value = f'W/"{value.value}"' if value.is_weak else f'"{value.value}"' self._headers[hdrs.ETAG] = hdr_value else: raise ValueError(f"Unsupported etag type: {type(value)}. " f"etag must be str, ETag or None") def _generate_content_type_header(self, CONTENT_TYPE: istr = hdrs.CONTENT_TYPE ) -> None: assert self._content_dict is not None assert self._content_type is not None params = "; ".join(f"{k}={v}" for k, v in self._content_dict.items()) if params: ctype = self._content_type + "; " + params else: ctype = self._content_type self._headers[CONTENT_TYPE] = ctype async def _do_start_compression(self, coding: ContentCoding) -> None: if coding != ContentCoding.identity: assert self._payload_writer is not None self._headers[hdrs.CONTENT_ENCODING] = coding.value self._payload_writer.enable_compression(coding.value) # Compressed payload may have different content length, # remove the header self._headers.popall(hdrs.CONTENT_LENGTH, None) async def _start_compression(self, request: "BaseRequest") -> None: if self._compression_force: await self._do_start_compression(self._compression_force) else: accept_encoding = request.headers.get(hdrs.ACCEPT_ENCODING, "").lower() for coding in ContentCoding: if coding.value in accept_encoding: await self._do_start_compression(coding) return async def prepare( self, request: "BaseRequest") -> Optional[AbstractStreamWriter]: if self._eof_sent: return None if self._payload_writer is not None: return self._payload_writer return await self._start(request) async def _start(self, request: "BaseRequest") -> AbstractStreamWriter: self._req = request writer = self._payload_writer = request._payload_writer await self._prepare_headers() await request._prepare_hook(self) await self._write_headers() return writer async def _prepare_headers(self) -> None: request = self._req assert request is not None writer = self._payload_writer assert writer is not None keep_alive = self._keep_alive if keep_alive is None: keep_alive = request.keep_alive self._keep_alive = keep_alive version = request.version headers = self._headers populate_with_cookies(headers, self.cookies) if self._compression: await self._start_compression(request) if self._chunked: if version != HttpVersion11: raise RuntimeError("Using chunked encoding is forbidden " "for HTTP/{0.major}.{0.minor}".format( request.version)) writer.enable_chunking() headers[hdrs.TRANSFER_ENCODING] = "chunked" if hdrs.CONTENT_LENGTH in headers: del headers[hdrs.CONTENT_LENGTH] elif self._length_check: writer.length = self.content_length if writer.length is None: if version >= HttpVersion11 and self.status != 204: writer.enable_chunking() headers[hdrs.TRANSFER_ENCODING] = "chunked" if hdrs.CONTENT_LENGTH in headers: del headers[hdrs.CONTENT_LENGTH] else: keep_alive = False # HTTP 1.1: https://tools.ietf.org/html/rfc7230#section-3.3.2 # HTTP 1.0: https://tools.ietf.org/html/rfc1945#section-10.4 elif version >= HttpVersion11 and self.status in (100, 101, 102, 103, 204): del headers[hdrs.CONTENT_LENGTH] if self.status not in (204, 304): headers.setdefault(hdrs.CONTENT_TYPE, "application/octet-stream") headers.setdefault(hdrs.DATE, rfc822_formatted_time()) headers.setdefault(hdrs.SERVER, SERVER_SOFTWARE) # connection header if hdrs.CONNECTION not in headers: if keep_alive: if version == HttpVersion10: headers[hdrs.CONNECTION] = "keep-alive" else: if version == HttpVersion11: headers[hdrs.CONNECTION] = "close" async def _write_headers(self) -> None: request = self._req assert request is not None writer = self._payload_writer assert writer is not None # status line version = request.version status_line = "HTTP/{}.{} {} {}".format(version[0], version[1], self._status, self._reason) await writer.write_headers(status_line, self._headers) async def write(self, data: bytes) -> None: assert isinstance( data, (bytes, bytearray, memoryview)), "data argument must be byte-ish (%r)" % type(data) if self._eof_sent: raise RuntimeError("Cannot call write() after write_eof()") if self._payload_writer is None: raise RuntimeError("Cannot call write() before prepare()") await self._payload_writer.write(data) async def drain(self) -> None: assert not self._eof_sent, "EOF has already been sent" assert self._payload_writer is not None, "Response has not been started" warnings.warn( "drain method is deprecated, use await resp.write()", DeprecationWarning, stacklevel=2, ) await self._payload_writer.drain() async def write_eof(self, data: bytes = b"") -> None: assert isinstance( data, (bytes, bytearray, memoryview)), "data argument must be byte-ish (%r)" % type(data) if self._eof_sent: return assert self._payload_writer is not None, "Response has not been started" await self._payload_writer.write_eof(data) self._eof_sent = True self._req = None self._body_length = self._payload_writer.output_size self._payload_writer = None def __repr__(self) -> str: if self._eof_sent: info = "eof" elif self.prepared: assert self._req is not None info = f"{self._req.method} {self._req.path} " else: info = "not prepared" return f"<{self.__class__.__name__} {self.reason} {info}>" def __getitem__(self, key: str) -> Any: return self._state[key] def __setitem__(self, key: str, value: Any) -> None: self._state[key] = value def __delitem__(self, key: str) -> None: del self._state[key] def __len__(self) -> int: return len(self._state) def __iter__(self) -> Iterator[str]: return iter(self._state) def __hash__(self) -> int: return hash(id(self)) def __eq__(self, other: object) -> bool: return self is other
class StreamingHTTPResponse(BaseHTTPResponse): __slots__ = ('transport', 'streaming_fn', 'status', 'content_type', 'headers', '_cookies') def __init__(self, streaming_fn, status=200, headers=None, content_type='text/plain'): self.content_type = content_type self.streaming_fn = streaming_fn self.status = status self.headers = CIMultiDict(headers or {}) self._cookies = None def write(self, data): """Writes a chunk of data to the streaming response. :param data: bytes-ish data to be written. """ if type(data) != bytes: data = self._encode_body(data) self.transport.write(b"%x\r\n%b\r\n" % (len(data), data)) async def stream(self, version="1.1", keep_alive=False, keep_alive_timeout=None): """Streams headers, runs the `streaming_fn` callback that writes content to the response body, then finalizes the response body. """ headers = self.get_headers(version, keep_alive=keep_alive, keep_alive_timeout=keep_alive_timeout) self.transport.write(headers) await self.streaming_fn(self) self.transport.write(b'0\r\n\r\n') def get_headers(self, version="1.1", keep_alive=False, keep_alive_timeout=None): # This is all returned in a kind-of funky way # We tried to make this as fast as possible in pure python timeout_header = b'' if keep_alive and keep_alive_timeout is not None: timeout_header = b'Keep-Alive: %d\r\n' % keep_alive_timeout self.headers['Transfer-Encoding'] = 'chunked' self.headers.pop('Content-Length', None) self.headers['Content-Type'] = self.headers.get( 'Content-Type', self.content_type) headers = self._parse_headers() if self.status is 200: status = b'OK' else: status = http.STATUS_CODES.get(self.status) return (b'HTTP/%b %d %b\r\n' b'%b' b'%b\r\n') % (version.encode(), self.status, status, timeout_header, headers)
class StreamResponse(BaseClass, HeadersMixin): __slots__ = ('_length_check', '_body', '_keep_alive', '_chunked', '_compression', '_compression_force', '_cookies', '_req', '_payload_writer', '_eof_sent', '_body_length', '_state', '_headers', '_status', '_reason') def __init__(self, *, status: int = 200, reason: Optional[str] = None, headers: Optional[LooseHeaders] = None) -> None: super().__init__() self._length_check = True self._body = None self._keep_alive = None # type: Optional[bool] self._chunked = False self._compression = False self._compression_force = None # type: Optional[ContentCoding] self._cookies = SimpleCookie() self._req = None # type: Optional[BaseRequest] self._payload_writer = None # type: Optional[AbstractStreamWriter] self._eof_sent = False self._body_length = 0 self._state = {} # type: Dict[str, Any] if headers is not None: self._headers = CIMultiDict(headers) # type: CIMultiDict[str] else: self._headers = CIMultiDict() # type: CIMultiDict[str] self.set_status(status, reason) @property def prepared(self) -> bool: return self._payload_writer is not None @property def task(self) -> 'asyncio.Task[None]': return getattr(self._req, 'task', None) @property def status(self) -> int: return self._status @property def chunked(self) -> bool: return self._chunked @property def compression(self) -> bool: return self._compression @property def reason(self) -> str: return self._reason def set_status( self, status: int, reason: Optional[str] = None, _RESPONSES: Mapping[int, Tuple[str, str]] = RESPONSES) -> None: assert not self.prepared, \ 'Cannot change the response status code after ' \ 'the headers have been sent' self._status = int(status) if reason is None: try: reason = _RESPONSES[self._status][0] except Exception: reason = '' self._reason = reason @property def keep_alive(self) -> Optional[bool]: return self._keep_alive def force_close(self) -> None: self._keep_alive = False @property def body_length(self) -> int: return self._body_length def enable_chunked_encoding(self) -> None: """Enables automatic chunked transfer encoding.""" self._chunked = True if hdrs.CONTENT_LENGTH in self._headers: raise RuntimeError("You can't enable chunked encoding when " "a content length is set") def enable_compression(self, force: Optional[ContentCoding] = None) -> None: """Enables response compression encoding.""" # Backwards compatibility for when force was a bool <0.17. self._compression = True self._compression_force = force @property def headers(self) -> 'CIMultiDict[str]': return self._headers @property def cookies(self) -> SimpleCookie: return self._cookies def set_cookie(self, name: str, value: str, *, expires: Optional[str] = None, domain: Optional[str] = None, max_age: Optional[Union[int, str]] = None, path: str = '/', secure: Optional[str] = None, httponly: Optional[str] = None, version: Optional[str] = None) -> None: """Set or update response cookie. Sets new cookie or updates existent with new value. Also updates only those params which are not None. """ old = self._cookies.get(name) if old is not None and old.coded_value == '': # deleted cookie self._cookies.pop(name, None) self._cookies[name] = value c = self._cookies[name] if expires is not None: c['expires'] = expires elif c.get('expires') == 'Thu, 01 Jan 1970 00:00:00 GMT': del c['expires'] if domain is not None: c['domain'] = domain if max_age is not None: c['max-age'] = str(max_age) elif 'max-age' in c: del c['max-age'] c['path'] = path if secure is not None: c['secure'] = secure if httponly is not None: c['httponly'] = httponly if version is not None: c['version'] = version def del_cookie(self, name: str, *, domain: Optional[str] = None, path: str = '/') -> None: """Delete cookie. Creates new empty expired cookie. """ # TODO: do we need domain/path here? self._cookies.pop(name, None) self.set_cookie(name, '', max_age=0, expires="Thu, 01 Jan 1970 00:00:00 GMT", domain=domain, path=path) @property def content_length(self) -> Optional[int]: # Just a placeholder for adding setter return super().content_length @content_length.setter def content_length(self, value: Optional[int]) -> None: if value is not None: value = int(value) if self._chunked: raise RuntimeError("You can't set content length when " "chunked encoding is enable") self._headers[hdrs.CONTENT_LENGTH] = str(value) else: self._headers.pop(hdrs.CONTENT_LENGTH, None) @property def content_type(self) -> str: # Just a placeholder for adding setter return super().content_type @content_type.setter def content_type(self, value: str) -> None: self.content_type # read header values if needed self._content_type = str(value) self._generate_content_type_header() @property def charset(self) -> Optional[str]: # Just a placeholder for adding setter return super().charset @charset.setter def charset(self, value: Optional[str]) -> None: ctype = self.content_type # read header values if needed if ctype == 'application/octet-stream': raise RuntimeError("Setting charset for application/octet-stream " "doesn't make sense, setup content_type first") assert self._content_dict is not None if value is None: self._content_dict.pop('charset', None) else: self._content_dict['charset'] = str(value).lower() self._generate_content_type_header() @property def last_modified(self) -> Optional[datetime.datetime]: """The value of Last-Modified HTTP header, or None. This header is represented as a `datetime` object. """ httpdate = self._headers.get(hdrs.LAST_MODIFIED) if httpdate is not None: timetuple = parsedate(httpdate) if timetuple is not None: return datetime.datetime(*timetuple[:6], tzinfo=datetime.timezone.utc) return None @last_modified.setter def last_modified( self, value: Optional[Union[int, float, datetime.datetime, str]]) -> None: if value is None: self._headers.pop(hdrs.LAST_MODIFIED, None) elif isinstance(value, (int, float)): self._headers[hdrs.LAST_MODIFIED] = time.strftime( "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(math.ceil(value))) elif isinstance(value, datetime.datetime): self._headers[hdrs.LAST_MODIFIED] = time.strftime( "%a, %d %b %Y %H:%M:%S GMT", value.utctimetuple()) elif isinstance(value, str): self._headers[hdrs.LAST_MODIFIED] = value def _generate_content_type_header(self, CONTENT_TYPE: istr = hdrs.CONTENT_TYPE ) -> None: assert self._content_dict is not None assert self._content_type is not None params = '; '.join("{}={}".format(k, v) for k, v in self._content_dict.items()) if params: ctype = self._content_type + '; ' + params else: ctype = self._content_type self._headers[CONTENT_TYPE] = ctype async def _do_start_compression(self, coding: ContentCoding) -> None: if coding != ContentCoding.identity: assert self._payload_writer is not None self._headers[hdrs.CONTENT_ENCODING] = coding.value self._payload_writer.enable_compression(coding.value) # Compressed payload may have different content length, # remove the header self._headers.popall(hdrs.CONTENT_LENGTH, None) async def _start_compression(self, request: 'BaseRequest') -> None: if self._compression_force: await self._do_start_compression(self._compression_force) else: accept_encoding = request.headers.get(hdrs.ACCEPT_ENCODING, '').lower() for coding in ContentCoding: if coding.value in accept_encoding: await self._do_start_compression(coding) return async def prepare( self, request: 'BaseRequest') -> Optional[AbstractStreamWriter]: if self._eof_sent: return None if self._payload_writer is not None: return self._payload_writer await request._prepare_hook(self) return await self._start(request) async def _start(self, request: 'BaseRequest') -> AbstractStreamWriter: self._req = request keep_alive = self._keep_alive if keep_alive is None: keep_alive = request.keep_alive self._keep_alive = keep_alive version = request.version writer = self._payload_writer = request._payload_writer headers = self._headers for cookie in self._cookies.values(): value = cookie.output(header='')[1:] headers.add(hdrs.SET_COOKIE, value) if self._compression: await self._start_compression(request) if self._chunked: if version != HttpVersion11: raise RuntimeError("Using chunked encoding is forbidden " "for HTTP/{0.major}.{0.minor}".format( request.version)) writer.enable_chunking() headers[hdrs.TRANSFER_ENCODING] = 'chunked' if hdrs.CONTENT_LENGTH in headers: del headers[hdrs.CONTENT_LENGTH] elif self._length_check: writer.length = self.content_length if writer.length is None: if version >= HttpVersion11: writer.enable_chunking() headers[hdrs.TRANSFER_ENCODING] = 'chunked' if hdrs.CONTENT_LENGTH in headers: del headers[hdrs.CONTENT_LENGTH] else: keep_alive = False headers.setdefault(hdrs.CONTENT_TYPE, 'application/octet-stream') headers.setdefault(hdrs.DATE, rfc822_formatted_time()) headers.setdefault(hdrs.SERVER, SERVER_SOFTWARE) # connection header if hdrs.CONNECTION not in headers: if keep_alive: if version == HttpVersion10: headers[hdrs.CONNECTION] = 'keep-alive' else: if version == HttpVersion11: headers[hdrs.CONNECTION] = 'close' # status line status_line = 'HTTP/{}.{} {} {}'.format(version[0], version[1], self._status, self._reason) await writer.write_headers(status_line, headers) return writer async def write(self, data: bytes) -> None: assert isinstance(data, (bytes, bytearray, memoryview)), \ "data argument must be byte-ish (%r)" % type(data) if self._eof_sent: raise RuntimeError("Cannot call write() after write_eof()") if self._payload_writer is None: raise RuntimeError("Cannot call write() before prepare()") await self._payload_writer.write(data) async def drain(self) -> None: assert not self._eof_sent, "EOF has already been sent" assert self._payload_writer is not None, \ "Response has not been started" warnings.warn("drain method is deprecated, use await resp.write()", DeprecationWarning, stacklevel=2) await self._payload_writer.drain() async def write_eof(self, data: bytes = b'') -> None: assert isinstance(data, (bytes, bytearray, memoryview)), \ "data argument must be byte-ish (%r)" % type(data) if self._eof_sent: return assert self._payload_writer is not None, \ "Response has not been started" await self._payload_writer.write_eof(data) self._eof_sent = True self._req = None self._body_length = self._payload_writer.output_size self._payload_writer = None def __repr__(self) -> str: if self._eof_sent: info = "eof" elif self.prepared: assert self._req is not None info = "{} {} ".format(self._req.method, self._req.path) else: info = "not prepared" return "<{} {} {}>".format(self.__class__.__name__, self.reason, info) def __getitem__(self, key: str) -> Any: return self._state[key] def __setitem__(self, key: str, value: Any) -> None: self._state[key] = value def __delitem__(self, key: str) -> None: del self._state[key] def __len__(self) -> int: return len(self._state) def __iter__(self) -> Iterator[str]: return iter(self._state) def __hash__(self) -> int: return hash(id(self)) def __eq__(self, other: object) -> bool: return self is other
class HttpRequest(RequestBase): """An :class:`HttpClient` request for an HTTP resource. This class has a similar interface to :class:`urllib.request.Request`. :param files: optional dictionary of name, file-like-objects. :param allow_redirects: allow the response to follow redirects. .. attribute:: method The request method .. attribute:: version HTTP version for this request, usually ``HTTP/1.1`` .. attribute:: history List of past :class:`.HttpResponse` (collected during redirects). .. attribute:: wait_continue if ``True``, the :class:`HttpRequest` includes the ``Expect: 100-Continue`` header. .. attribute:: stream Allow for streaming body """ _proxy = None _ssl = None _tunnel = None _write_done = False def __init__(self, client, url, method, inp_params=None, headers=None, data=None, files=None, json=None, history=None, auth=None, charset=None, max_redirects=10, source_address=None, allow_redirects=False, decompress=True, version=None, wait_continue=False, websocket_handler=None, cookies=None, params=None, stream=False, proxies=None, verify=True, cert=None, **extra): self.client = client self.method = method.upper() self.inp_params = inp_params or {} self.unredirected_headers = CIMultiDict() self.history = history self.wait_continue = wait_continue self.max_redirects = max_redirects self.allow_redirects = allow_redirects self.charset = charset or 'utf-8' self.version = version self.decompress = decompress self.websocket_handler = websocket_handler self.source_address = source_address self.stream = stream self.verify = verify self.cert = cert if auth and not isinstance(auth, Auth): auth = HTTPBasicAuth(*auth) self.auth = auth self.url = full_url(url, params, method=self.method) self._set_proxy(proxies) self.key = RequestKey.create(self) self.headers = client.get_headers(self, headers) self.body = self._encode_body(data, files, json) self.unredirected_headers['host'] = self.key.netloc cookies = cookiejar_from_dict(client.cookies, cookies) if cookies: cookies.add_cookie_header(self) @property def _loop(self): return self.client._loop @property def ssl(self): """Context for TLS connections. If this is a tunneled request and the tunnel connection is not yet established, it returns ``None``. """ return self._ssl @property def proxy(self): """Proxy server for this request. """ return self._proxy @property def tunnel(self): """Tunnel for this request. """ return self._tunnel def __repr__(self): return self.first_line() __str__ = __repr__ def first_line(self): if self.method == 'CONNECT': url = self.key.netloc elif self._proxy: url = self.url else: p = urlparse(self.url) url = urlunparse(('', '', p.path or '/', p.params, p.query, p.fragment)) return '%s %s %s' % (self.method, url, self.version) def is_chunked(self): return self.body and 'content-length' not in self.headers def encode(self): """The bytes representation of this :class:`HttpRequest`. Called by :class:`HttpResponse` when it needs to encode this :class:`HttpRequest` before sending it to the HTTP resource. """ # Call body before fist_line in case the query is changes. first_line = self.first_line() if self.body and self.wait_continue: self.headers['expect'] = '100-continue' headers = self.headers if self.unredirected_headers: headers = self.unredirected_headers.copy() headers.update(self.headers) buffer = [first_line.encode('ascii'), b'\r\n'] buffer.extend((('%s: %s\r\n' % (name, value)).encode(CHARSET) for name, value in headers.items())) buffer.append(b'\r\n') return b''.join(buffer) def add_header(self, key, value): self.headers[key] = value def has_header(self, header_name): """Check ``header_name`` is in this request headers. """ return (header_name in self.headers or header_name in self.unredirected_headers) def get_header(self, header_name, default=None): """Retrieve ``header_name`` from this request headers. """ return self.headers.get( header_name, self.unredirected_headers.get(header_name, default)) def remove_header(self, header_name): """Remove ``header_name`` from this request. """ val1 = self.headers.pop(header_name, None) val2 = self.unredirected_headers.pop(header_name, None) return val1 or val2 def add_unredirected_header(self, header_name, header_value): self.unredirected_headers[header_name] = header_value def write_body(self, transport): assert not self._write_done, 'Body already sent' self._write_done = True if not self.body: return if is_streamed(self.body): self._loop.create_task(self._write_streamed_data(transport)) else: self._write_body_data(transport, self.body, True) # INTERNAL ENCODING METHODS def _encode_body(self, data, files, json): body = None ct = None if isinstance(data, (str, bytes)): if files: raise ValueError('data cannot be a string or bytes when ' 'files are present') body = to_bytes(data, self.charset) elif data and is_streamed(data): if files: raise ValueError('data cannot be an iterator when ' 'files are present') if 'content-length' not in self.headers: self.headers['transfer-encoding'] = 'chunked' return data elif data or files: if files: body, ct = self._encode_files(data, files) else: body, ct = self._encode_params(data) elif json: body = _json.dumps(json).encode(self.charset) ct = 'application/json' if not self.headers.get('content-type') and ct: self.headers['Content-Type'] = ct if body: self.headers['content-length'] = str(len(body)) return body def _encode_files(self, data, files): fields = [] for field, val in mapping_iterator(data or ()): if (isinstance(val, str) or isinstance(val, bytes) or not hasattr(val, '__iter__')): val = [val] for v in val: if v is not None: if not isinstance(v, bytes): v = str(v) fields.append((field.decode('utf-8') if isinstance(field, bytes) else field, v.encode('utf-8') if isinstance(v, str) else v)) for (k, v) in mapping_iterator(files): # support for explicit filename ft = None if isinstance(v, (tuple, list)): if len(v) == 2: fn, fp = v else: fn, fp, ft = v else: fn = guess_filename(v) or k fp = v if isinstance(fp, bytes): fp = BytesIO(fp) elif isinstance(fp, str): fp = StringIO(fp) if ft: new_v = (fn, fp.read(), ft) else: new_v = (fn, fp.read()) fields.append((k, new_v)) # return encode_multipart_formdata(fields, charset=self.charset) def _encode_params(self, params): content_type = self.headers.get('content-type') # No content type given, chose one if not content_type: content_type = FORM_URL_ENCODED if hasattr(params, 'read'): params = params.read() if content_type in JSON_CONTENT_TYPES: body = _json.dumps(params) elif content_type == FORM_URL_ENCODED: body = urlencode(tuple(split_url_params(params))) elif content_type == MULTIPART_FORM_DATA: body, content_type = encode_multipart_formdata( params, charset=self.charset) else: body = params return to_bytes(body, self.charset), content_type def _write_body_data(self, transport, data, finish=False): if self.is_chunked(): data = http_chunks(data, finish) elif data: data = (data,) else: return for chunk in data: transport.write(chunk) async def _write_streamed_data(self, transport): for data in self.body: try: data = await data except TypeError: pass self._write_body_data(transport, data) self._write_body_data(transport, b'', True) # PROXY INTERNALS def _set_proxy(self, proxies): url = urlparse(self.url) request_proxies = self.client.proxies.copy() if proxies: request_proxies.update(proxies) self.proxies = request_proxies # if url.scheme in request_proxies: host, port = get_hostport(url.scheme, url.netloc) no_proxy = [n for n in request_proxies.get('no', '').split(',') if n] if not any(map(host.endswith, no_proxy)): proxy_url = request_proxies[url.scheme] if url.scheme in tls_schemes: self._tunnel = proxy_url else: self._proxy = proxy_url
class Request: ''' The API request object. ''' __slots__ = ( 'config', 'session', 'method', 'path', 'date', 'headers', 'params', 'content_type', '_content', '_attached_files', 'reporthook', ) _allowed_methods = frozenset( ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']) def __init__(self, session: BaseSession, method: str = 'GET', path: str = None, content: RequestContent = None, *, content_type: str = None, params: Mapping[str, str] = None, reporthook: Callable = None) -> None: ''' Initialize an API request. :param BaseSession session: The session where this request is executed on. :param str path: The query path. When performing requests, the version number prefix will be automatically perpended if required. :param RequestContent content: The API query body which will be encoded as JSON. :param str content_type: Explicitly set the content type. See also :func:`Request.set_content`. ''' self.session = session self.config = session.config self.method = method if path.startswith('/'): path = path[1:] self.path = path self.params = params self.date = None self.headers = CIMultiDict([ ('User-Agent', self.config.user_agent), ('X-BackendAI-Domain', self.config.domain), ('X-BackendAI-Version', self.config.version), ]) self._attached_files = None self.set_content(content, content_type=content_type) self.reporthook = reporthook @property def content(self) -> RequestContent: ''' Retrieves the content in the original form. Private codes should NOT use this as it incurs duplicate encoding/decoding. ''' return self._content def set_content(self, value: RequestContent, *, content_type: str = None): ''' Sets the content of the request. ''' assert self._attached_files is None, \ 'cannot set content because you already attached files.' guessed_content_type = 'application/octet-stream' if value is None: guessed_content_type = 'text/plain' self._content = b'' elif isinstance(value, str): guessed_content_type = 'text/plain' self._content = value.encode('utf-8') else: guessed_content_type = 'application/octet-stream' self._content = value self.content_type = (content_type if content_type is not None else guessed_content_type) def set_json(self, value: object): ''' A shortcut for set_content() with JSON objects. ''' self.set_content(modjson.dumps(value, cls=ExtendedJSONEncoder), content_type='application/json') def attach_files(self, files: Sequence[AttachedFile]): ''' Attach a list of files represented as AttachedFile. ''' assert not self._content, 'content must be empty to attach files.' self.content_type = 'multipart/form-data' self._attached_files = files def _sign(self, rel_url, access_key=None, secret_key=None, hash_type=None): ''' Calculates the signature of the given request and adds the Authorization HTTP header. It should be called at the very end of request preparation and before sending the request to the server. ''' if access_key is None: access_key = self.config.access_key if secret_key is None: secret_key = self.config.secret_key if hash_type is None: hash_type = self.config.hash_type if self.config.endpoint_type == 'api': hdrs, _ = generate_signature(self.method, self.config.version, self.config.endpoint, self.date, str(rel_url), self.content_type, self._content, access_key, secret_key, hash_type) self.headers.update(hdrs) elif self.config.endpoint_type == 'session': local_state_path = Path( appdirs.user_state_dir('backend.ai', 'Lablup')) try: self.session.aiohttp_session.cookie_jar.load(local_state_path / 'cookie.dat') except (IOError, PermissionError): pass else: raise ValueError('unsupported endpoint type') def _pack_content(self): if self._attached_files is not None: data = aiohttp.FormData() for f in self._attached_files: data.add_field('src', f.stream, filename=f.filename, content_type=f.content_type) assert data.is_multipart, 'Failed to pack files as multipart.' # Let aiohttp fill up the content-type header including # multipart boundaries. self.headers.pop('Content-Type', None) return data else: return self._content def _build_url(self): base_url = self.config.endpoint.path.rstrip('/') query_path = self.path.lstrip('/') if len(self.path) > 0 else '' if self.config.endpoint_type == 'session': if not query_path.startswith('server'): query_path = 'func/{0}'.format(query_path) path = '{0}/{1}'.format(base_url, query_path) url = self.config.endpoint.with_path(path) if self.params: url = url.with_query(self.params) return url # TODO: attach rate-limit information def fetch(self, **kwargs) -> 'FetchContextManager': ''' Sends the request to the server and reads the response. You may use this method either with plain synchronous Session or AsyncSession. Both the followings patterns are valid: .. code-block:: python3 from ai.backend.client.request import Request from ai.backend.client.session import Session with Session() as sess: rqst = Request(sess, 'GET', ...) with rqst.fetch() as resp: print(resp.text()) .. code-block:: python3 from ai.backend.client.request import Request from ai.backend.client.session import AsyncSession async with AsyncSession() as sess: rqst = Request(sess, 'GET', ...) async with rqst.fetch() as resp: print(await resp.text()) ''' assert self.method in self._allowed_methods, \ 'Disallowed HTTP method: {}'.format(self.method) self.date = datetime.now(tzutc()) self.headers['Date'] = self.date.isoformat() if self.content_type is not None and 'Content-Type' not in self.headers: self.headers['Content-Type'] = self.content_type force_anonymous = kwargs.pop('anonymous', False) def _rqst_ctx_builder(): timeout_config = aiohttp.ClientTimeout( total=None, connect=None, sock_connect=self.config.connection_timeout, sock_read=self.config.read_timeout, ) full_url = self._build_url() if not self.config.is_anonymous and not force_anonymous: self._sign(full_url.relative()) return self.session.aiohttp_session.request( self.method, str(full_url), data=self._pack_content(), timeout=timeout_config, headers=self.headers) return FetchContextManager(self.session, _rqst_ctx_builder, **kwargs) def connect_websocket(self, **kwargs) -> 'WebSocketContextManager': ''' Creates a WebSocket connection. .. warning:: This method only works with :class:`~ai.backend.client.session.AsyncSession`. ''' assert isinstance(self.session, AsyncSession), \ 'Cannot use websockets with sessions in the synchronous mode' assert self.method == 'GET', 'Invalid websocket method' self.date = datetime.now(tzutc()) self.headers['Date'] = self.date.isoformat() # websocket is always a "binary" stream. self.content_type = 'application/octet-stream' def _ws_ctx_builder(): full_url = self._build_url() if not self.config.is_anonymous: self._sign(full_url.relative()) return self.session.aiohttp_session.ws_connect( str(full_url), autoping=True, heartbeat=30.0, headers=self.headers) return WebSocketContextManager(self.session, _ws_ctx_builder, **kwargs) def connect_events(self, **kwargs) -> 'SSEContextManager': ''' Creates a Server-Sent Events connection. .. warning:: This method only works with :class:`~ai.backend.client.session.AsyncSession`. ''' assert isinstance(self.session, AsyncSession), \ 'Cannot use event streams with sessions in the synchronous mode' assert self.method == 'GET', 'Invalid event stream method' self.date = datetime.now(tzutc()) self.headers['Date'] = self.date.isoformat() self.content_type = 'application/octet-stream' def _rqst_ctx_builder(): timeout_config = aiohttp.ClientTimeout( total=None, connect=None, sock_connect=self.config.connection_timeout, sock_read=self.config.read_timeout, ) full_url = self._build_url() if not self.config.is_anonymous: self._sign(full_url.relative()) return self.session.aiohttp_session.request(self.method, str(full_url), timeout=timeout_config, headers=self.headers) return SSEContextManager(self.session, _rqst_ctx_builder, **kwargs)
class HttpRequest(RequestBase): """An :class:`HttpClient` request for an HTTP resource. This class has a similar interface to :class:`urllib.request.Request`. :param files: optional dictionary of name, file-like-objects. :param allow_redirects: allow the response to follow redirects. .. attribute:: method The request method .. attribute:: version HTTP version for this request, usually ``HTTP/1.1`` .. attribute:: history List of past :class:`.HttpResponse` (collected during redirects). .. attribute:: wait_continue if ``True``, the :class:`HttpRequest` includes the ``Expect: 100-Continue`` header. .. attribute:: stream Allow for streaming body """ _proxy = None _ssl = None _tunnel = None _write_done = False def __init__(self, client, url, method, inp_params=None, headers=None, data=None, files=None, json=None, history=None, auth=None, charset=None, max_redirects=10, source_address=None, allow_redirects=False, decompress=True, version=None, wait_continue=False, websocket_handler=None, cookies=None, params=None, stream=False, proxies=None, verify=True, cert=None, **extra): self.client = client self.method = method.upper() self.inp_params = inp_params or {} self.unredirected_headers = CIMultiDict() self.history = history self.wait_continue = wait_continue self.max_redirects = max_redirects self.allow_redirects = allow_redirects self.charset = charset or 'utf-8' self.version = version self.decompress = decompress self.websocket_handler = websocket_handler self.source_address = source_address self.stream = stream self.verify = verify self.cert = cert if auth and not isinstance(auth, Auth): auth = HTTPBasicAuth(*auth) self.auth = auth self.url = full_url(url, params, method=self.method) self._set_proxy(proxies) self.key = RequestKey.create(self) self.headers = client.get_headers(self, headers) self.body = self._encode_body(data, files, json) self.unredirected_headers['host'] = self.key.netloc cookies = cookiejar_from_dict(client.cookies, cookies) if cookies: cookies.add_cookie_header(self) @property def _loop(self): return self.client._loop @property def ssl(self): """Context for TLS connections. If this is a tunneled request and the tunnel connection is not yet established, it returns ``None``. """ return self._ssl @property def proxy(self): """Proxy server for this request. """ return self._proxy @property def tunnel(self): """Tunnel for this request. """ return self._tunnel def __repr__(self): return self.first_line() __str__ = __repr__ def first_line(self): if self.method == 'CONNECT': url = self.key.netloc elif self._proxy: url = self.url else: p = urlparse(self.url) url = urlunparse(('', '', p.path or '/', p.params, p.query, p.fragment)) return '%s %s %s' % (self.method, url, self.version) def is_chunked(self): return self.body and 'content-length' not in self.headers def encode(self): """The bytes representation of this :class:`HttpRequest`. Called by :class:`HttpResponse` when it needs to encode this :class:`HttpRequest` before sending it to the HTTP resource. """ # Call body before fist_line in case the query is changes. first_line = self.first_line() if self.body and self.wait_continue: self.headers['expect'] = '100-continue' headers = self.headers if self.unredirected_headers: headers = self.unredirected_headers.copy() headers.update(self.headers) buffer = [first_line.encode('ascii'), b'\r\n'] buffer.extend((('%s: %s\r\n' % (name, value)).encode(CHARSET) for name, value in headers.items())) buffer.append(b'\r\n') return b''.join(buffer) def add_header(self, key, value): self.headers[key] = value def has_header(self, header_name): """Check ``header_name`` is in this request headers. """ return (header_name in self.headers or header_name in self.unredirected_headers) def get_header(self, header_name, default=None): """Retrieve ``header_name`` from this request headers. """ return self.headers.get( header_name, self.unredirected_headers.get(header_name, default)) def remove_header(self, header_name): """Remove ``header_name`` from this request. """ val1 = self.headers.pop(header_name, None) val2 = self.unredirected_headers.pop(header_name, None) return val1 or val2 def add_unredirected_header(self, header_name, header_value): self.unredirected_headers[header_name] = header_value def write_body(self, transport): assert not self._write_done, 'Body already sent' self._write_done = True if not self.body: return if is_streamed(self.body): self._loop.create_task(self._write_streamed_data(transport)) else: self._write_body_data(transport, self.body, True) # INTERNAL ENCODING METHODS def _encode_body(self, data, files, json): body = None ct = None if isinstance(data, (str, bytes)): if files: raise ValueError('data cannot be a string or bytes when ' 'files are present') body = to_bytes(data, self.charset) elif data and is_streamed(data): if files: raise ValueError('data cannot be an iterator when ' 'files are present') if 'content-length' not in self.headers: self.headers['transfer-encoding'] = 'chunked' return data elif data or files: if files: body, ct = self._encode_files(data, files) else: body, ct = self._encode_params(data) elif json: body = _json.dumps(json).encode(self.charset) ct = 'application/json' if not self.headers.get('content-type') and ct: self.headers['Content-Type'] = ct if body: self.headers['content-length'] = str(len(body)) return body def _encode_files(self, data, files): fields = [] for field, val in mapping_iterator(data or ()): if (isinstance(val, str) or isinstance(val, bytes) or not hasattr(val, '__iter__')): val = [val] for v in val: if v is not None: if not isinstance(v, bytes): v = str(v) fields.append( (field.decode('utf-8') if isinstance(field, bytes) else field, v.encode('utf-8') if isinstance(v, str) else v)) for (k, v) in mapping_iterator(files): # support for explicit filename ft = None if isinstance(v, (tuple, list)): if len(v) == 2: fn, fp = v else: fn, fp, ft = v else: fn = guess_filename(v) or k fp = v if isinstance(fp, bytes): fp = BytesIO(fp) elif isinstance(fp, str): fp = StringIO(fp) if ft: new_v = (fn, fp.read(), ft) else: new_v = (fn, fp.read()) fields.append((k, new_v)) # return encode_multipart_formdata(fields, charset=self.charset) def _encode_params(self, params): content_type = self.headers.get('content-type') # No content type given, chose one if not content_type: content_type = FORM_URL_ENCODED if hasattr(params, 'read'): params = params.read() if content_type in JSON_CONTENT_TYPES: body = _json.dumps(params) elif content_type == FORM_URL_ENCODED: body = urlencode(tuple(split_url_params(params))) elif content_type == MULTIPART_FORM_DATA: body, content_type = encode_multipart_formdata( params, charset=self.charset) else: body = params return to_bytes(body, self.charset), content_type def _write_body_data(self, transport, data, finish=False): if self.is_chunked(): data = http_chunks(data, finish) elif data: data = (data, ) else: return for chunk in data: transport.write(chunk) async def _write_streamed_data(self, transport): for data in self.body: try: data = await data except TypeError: pass self._write_body_data(transport, data) self._write_body_data(transport, b'', True) # PROXY INTERNALS def _set_proxy(self, proxies): url = urlparse(self.url) request_proxies = self.client.proxies.copy() if proxies: request_proxies.update(proxies) self.proxies = request_proxies # if url.scheme in request_proxies: host, port = get_hostport(url.scheme, url.netloc) no_proxy = [ n for n in request_proxies.get('no', '').split(',') if n ] if not any(map(host.endswith, no_proxy)): proxy_url = request_proxies[url.scheme] if url.scheme in tls_schemes: self._tunnel = proxy_url else: self._proxy = proxy_url
class StreamResponse(collections.MutableMapping, HeadersMixin): _length_check = True def __init__(self, *, status=200, reason=None, headers=None): self._body = None self._keep_alive = None self._chunked = False self._compression = False self._compression_force = False self._cookies = SimpleCookie() self._req = None self._payload_writer = None self._eof_sent = False self._body_length = 0 self._state = {} if headers is not None: self._headers = CIMultiDict(headers) else: self._headers = CIMultiDict() self.set_status(status, reason) @property def prepared(self): return self._payload_writer is not None @property def task(self): return getattr(self._req, 'task', None) @property def status(self): return self._status @property def chunked(self): return self._chunked @property def compression(self): return self._compression @property def reason(self): return self._reason def set_status(self, status, reason=None, _RESPONSES=RESPONSES): assert not self.prepared, \ 'Cannot change the response status code after ' \ 'the headers have been sent' self._status = int(status) if reason is None: try: reason = _RESPONSES[self._status][0] except Exception: reason = '' self._reason = reason @property def keep_alive(self): return self._keep_alive def force_close(self): self._keep_alive = False @property def body_length(self): return self._body_length @property def output_length(self): warnings.warn('output_length is deprecated', DeprecationWarning) return self._payload_writer.buffer_size def enable_chunked_encoding(self, chunk_size=None): """Enables automatic chunked transfer encoding.""" self._chunked = True if hdrs.CONTENT_LENGTH in self._headers: raise RuntimeError("You can't enable chunked encoding when " "a content length is set") if chunk_size is not None: warnings.warn('Chunk size is deprecated #1615', DeprecationWarning) def enable_compression(self, force=None): """Enables response compression encoding.""" # Backwards compatibility for when force was a bool <0.17. if type(force) == bool: force = ContentCoding.deflate if force else ContentCoding.identity elif force is not None: assert isinstance(force, ContentCoding), ("force should one of " "None, bool or " "ContentEncoding") self._compression = True self._compression_force = force @property def headers(self): return self._headers @property def cookies(self): return self._cookies def set_cookie(self, name, value, *, expires=None, domain=None, max_age=None, path='/', secure=None, httponly=None, version=None): """Set or update response cookie. Sets new cookie or updates existent with new value. Also updates only those params which are not None. """ old = self._cookies.get(name) if old is not None and old.coded_value == '': # deleted cookie self._cookies.pop(name, None) self._cookies[name] = value c = self._cookies[name] if expires is not None: c['expires'] = expires elif c.get('expires') == 'Thu, 01 Jan 1970 00:00:00 GMT': del c['expires'] if domain is not None: c['domain'] = domain if max_age is not None: c['max-age'] = max_age elif 'max-age' in c: del c['max-age'] c['path'] = path if secure is not None: c['secure'] = secure if httponly is not None: c['httponly'] = httponly if version is not None: c['version'] = version def del_cookie(self, name, *, domain=None, path='/'): """Delete cookie. Creates new empty expired cookie. """ # TODO: do we need domain/path here? self._cookies.pop(name, None) self.set_cookie(name, '', max_age=0, expires="Thu, 01 Jan 1970 00:00:00 GMT", domain=domain, path=path) @property def content_length(self): # Just a placeholder for adding setter return super().content_length @content_length.setter def content_length(self, value): if value is not None: value = int(value) if self._chunked: raise RuntimeError("You can't set content length when " "chunked encoding is enable") self._headers[hdrs.CONTENT_LENGTH] = str(value) else: self._headers.pop(hdrs.CONTENT_LENGTH, None) @property def content_type(self): # Just a placeholder for adding setter return super().content_type @content_type.setter def content_type(self, value): self.content_type # read header values if needed self._content_type = str(value) self._generate_content_type_header() @property def charset(self): # Just a placeholder for adding setter return super().charset @charset.setter def charset(self, value): ctype = self.content_type # read header values if needed if ctype == 'application/octet-stream': raise RuntimeError("Setting charset for application/octet-stream " "doesn't make sense, setup content_type first") if value is None: self._content_dict.pop('charset', None) else: self._content_dict['charset'] = str(value).lower() self._generate_content_type_header() @property def last_modified(self, _LAST_MODIFIED=hdrs.LAST_MODIFIED): """The value of Last-Modified HTTP header, or None. This header is represented as a `datetime` object. """ httpdate = self.headers.get(_LAST_MODIFIED) if httpdate is not None: timetuple = parsedate(httpdate) if timetuple is not None: return datetime.datetime(*timetuple[:6], tzinfo=datetime.timezone.utc) return None @last_modified.setter def last_modified(self, value): if value is None: self.headers.pop(hdrs.LAST_MODIFIED, None) elif isinstance(value, (int, float)): self.headers[hdrs.LAST_MODIFIED] = time.strftime( "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(math.ceil(value))) elif isinstance(value, datetime.datetime): self.headers[hdrs.LAST_MODIFIED] = time.strftime( "%a, %d %b %Y %H:%M:%S GMT", value.utctimetuple()) elif isinstance(value, str): self.headers[hdrs.LAST_MODIFIED] = value def _generate_content_type_header(self, CONTENT_TYPE=hdrs.CONTENT_TYPE): params = '; '.join("%s=%s" % i for i in self._content_dict.items()) if params: ctype = self._content_type + '; ' + params else: ctype = self._content_type self.headers[CONTENT_TYPE] = ctype def _do_start_compression(self, coding): if coding != ContentCoding.identity: self.headers[hdrs.CONTENT_ENCODING] = coding.value self._payload_writer.enable_compression(coding.value) # Compressed payload may have different content length, # remove the header self._headers.popall(hdrs.CONTENT_LENGTH, None) def _start_compression(self, request): if self._compression_force: self._do_start_compression(self._compression_force) else: accept_encoding = request.headers.get( hdrs.ACCEPT_ENCODING, '').lower() for coding in ContentCoding: if coding.value in accept_encoding: self._do_start_compression(coding) return async def prepare(self, request): if self._eof_sent: return if self._payload_writer is not None: return self._payload_writer await request._prepare_hook(self) return self._start(request) def _start(self, request, HttpVersion10=HttpVersion10, HttpVersion11=HttpVersion11, CONNECTION=hdrs.CONNECTION, DATE=hdrs.DATE, SERVER=hdrs.SERVER, CONTENT_TYPE=hdrs.CONTENT_TYPE, CONTENT_LENGTH=hdrs.CONTENT_LENGTH, SET_COOKIE=hdrs.SET_COOKIE, SERVER_SOFTWARE=SERVER_SOFTWARE, TRANSFER_ENCODING=hdrs.TRANSFER_ENCODING): self._req = request keep_alive = self._keep_alive if keep_alive is None: keep_alive = request.keep_alive self._keep_alive = keep_alive version = request.version writer = self._payload_writer = request._payload_writer headers = self._headers for cookie in self._cookies.values(): value = cookie.output(header='')[1:] headers.add(SET_COOKIE, value) if self._compression: self._start_compression(request) if self._chunked: if version != HttpVersion11: raise RuntimeError( "Using chunked encoding is forbidden " "for HTTP/{0.major}.{0.minor}".format(request.version)) writer.enable_chunking() headers[TRANSFER_ENCODING] = 'chunked' if CONTENT_LENGTH in headers: del headers[CONTENT_LENGTH] elif self._length_check: writer.length = self.content_length if writer.length is None: if version >= HttpVersion11: writer.enable_chunking() headers[TRANSFER_ENCODING] = 'chunked' if CONTENT_LENGTH in headers: del headers[CONTENT_LENGTH] else: keep_alive = False headers.setdefault(CONTENT_TYPE, 'application/octet-stream') headers.setdefault(DATE, rfc822_formatted_time()) headers.setdefault(SERVER, SERVER_SOFTWARE) # connection header if CONNECTION not in headers: if keep_alive: if version == HttpVersion10: headers[CONNECTION] = 'keep-alive' else: if version == HttpVersion11: headers[CONNECTION] = 'close' # status line status_line = 'HTTP/{}.{} {} {}\r\n'.format( version[0], version[1], self._status, self._reason) writer.write_headers(status_line, headers) return writer async def write(self, data): assert isinstance(data, (bytes, bytearray, memoryview)), \ "data argument must be byte-ish (%r)" % type(data) if self._eof_sent: raise RuntimeError("Cannot call write() after write_eof()") if self._payload_writer is None: raise RuntimeError("Cannot call write() before prepare()") await self._payload_writer.write(data) async def drain(self): assert not self._eof_sent, "EOF has already been sent" assert self._payload_writer is not None, \ "Response has not been started" warnings.warn("drain method is deprecated, use await resp.write()", DeprecationWarning, stacklevel=2) await self._payload_writer.drain() async def write_eof(self, data=b''): assert isinstance(data, (bytes, bytearray, memoryview)), \ "data argument must be byte-ish (%r)" % type(data) if self._eof_sent: return assert self._payload_writer is not None, \ "Response has not been started" await self._payload_writer.write_eof(data) self._eof_sent = True self._req = None self._body_length = self._payload_writer.output_size self._payload_writer = None def __repr__(self): if self._eof_sent: info = "eof" elif self.prepared: info = "{} {} ".format(self._req.method, self._req.path) else: info = "not prepared" return "<{} {} {}>".format(self.__class__.__name__, self.reason, info) def __getitem__(self, key): return self._state[key] def __setitem__(self, key, value): self._state[key] = value def __delitem__(self, key): del self._state[key] def __len__(self): return len(self._state) def __iter__(self): return iter(self._state) def __hash__(self): return hash(id(self))
class Request: ''' The API request object. ''' __slots__ = ( 'config', 'session', 'method', 'path', 'date', 'headers', 'params', 'content_type', '_content', '_attached_files', 'reporthook', ) _allowed_methods = frozenset([ 'GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']) def __init__(self, session: BaseSession, method: str = 'GET', path: str = None, content: RequestContent = None, *, content_type: str = None, params: Mapping[str, str] = None, reporthook: Callable = None) -> None: ''' Initialize an API request. :param BaseSession session: The session where this request is executed on. :param str path: The query path. When performing requests, the version number prefix will be automatically perpended if required. :param RequestContent content: The API query body which will be encoded as JSON. :param str content_type: Explicitly set the content type. See also :func:`Request.set_content`. ''' self.session = session self.config = session.config self.method = method if path.startswith('/'): path = path[1:] self.path = path self.params = params self.date = None self.headers = CIMultiDict([ ('User-Agent', self.config.user_agent), ('X-BackendAI-Version', self.config.version), ]) self._attached_files = None self.set_content(content, content_type=content_type) self.reporthook = reporthook @property def content(self) -> RequestContent: ''' Retrieves the content in the original form. Private codes should NOT use this as it incurs duplicate encoding/decoding. ''' return self._content def set_content(self, value: RequestContent, *, content_type: str = None): ''' Sets the content of the request. ''' assert self._attached_files is None, \ 'cannot set content because you already attached files.' guessed_content_type = 'application/octet-stream' if value is None: guessed_content_type = 'text/plain' self._content = b'' elif isinstance(value, str): guessed_content_type = 'text/plain' self._content = value.encode('utf-8') else: guessed_content_type = 'application/octet-stream' self._content = value self.content_type = (content_type if content_type is not None else guessed_content_type) def set_json(self, value: object): ''' A shortcut for set_content() with JSON objects. ''' self.set_content(modjson.dumps(value, cls=ExtendedJSONEncoder), content_type='application/json') def attach_files(self, files: Sequence[AttachedFile]): ''' Attach a list of files represented as AttachedFile. ''' assert not self._content, 'content must be empty to attach files.' self.content_type = 'multipart/form-data' self._attached_files = files def _sign(self, rel_url, access_key=None, secret_key=None, hash_type=None): ''' Calculates the signature of the given request and adds the Authorization HTTP header. It should be called at the very end of request preparation and before sending the request to the server. ''' if access_key is None: access_key = self.config.access_key if secret_key is None: secret_key = self.config.secret_key if hash_type is None: hash_type = self.config.hash_type hdrs, _ = generate_signature( self.method, self.config.version, self.config.endpoint, self.date, str(rel_url), self.content_type, self._content, access_key, secret_key, hash_type) self.headers.update(hdrs) def _pack_content(self): if self._attached_files is not None: data = aiohttp.FormData() for f in self._attached_files: data.add_field('src', f.stream, filename=f.filename, content_type=f.content_type) assert data.is_multipart, 'Failed to pack files as multipart.' # Let aiohttp fill up the content-type header including # multipart boundaries. self.headers.pop('Content-Type', None) return data else: return self._content def _build_url(self): base_url = self.config.endpoint.path.rstrip('/') query_path = self.path.lstrip('/') if len(self.path) > 0 else '' path = '{0}/{1}'.format(base_url, query_path) url = self.config.endpoint.with_path(path) if self.params: url = url.with_query(self.params) return url # TODO: attach rate-limit information def fetch(self, **kwargs) -> 'FetchContextManager': ''' Sends the request to the server and reads the response. You may use this method either with plain synchronous Session or AsyncSession. Both the followings patterns are valid: .. code-block:: python3 from ai.backend.client.request import Request from ai.backend.client.session import Session with Session() as sess: rqst = Request(sess, 'GET', ...) with rqst.fetch() as resp: print(resp.text()) .. code-block:: python3 from ai.backend.client.request import Request from ai.backend.client.session import AsyncSession async with AsyncSession() as sess: rqst = Request(sess, 'GET', ...) async with rqst.fetch() as resp: print(await resp.text()) ''' assert self.method in self._allowed_methods, \ 'Disallowed HTTP method: {}'.format(self.method) self.date = datetime.now(tzutc()) self.headers['Date'] = self.date.isoformat() if self.content_type is not None: self.headers['Content-Type'] = self.content_type full_url = self._build_url() self._sign(full_url.relative()) rqst_ctx = self.session.aiohttp_session.request( self.method, str(full_url), data=self._pack_content(), timeout=_default_request_timeout, headers=self.headers) return FetchContextManager(self.session, rqst_ctx, **kwargs) def connect_websocket(self, **kwargs) -> 'WebSocketContextManager': ''' Creates a WebSocket connection. .. warning:: This method only works with :class:`~ai.backend.client.session.AsyncSession`. ''' assert isinstance(self.session, AsyncSession), \ 'Cannot use websockets with sessions in the synchronous mode' assert self.method == 'GET', 'Invalid websocket method' self.date = datetime.now(tzutc()) self.headers['Date'] = self.date.isoformat() # websocket is always a "binary" stream. self.content_type = 'application/octet-stream' full_url = self._build_url() self._sign(full_url.relative()) ws_ctx = self.session.aiohttp_session.ws_connect( str(full_url), autoping=True, heartbeat=30.0, headers=self.headers) return WebSocketContextManager(self.session, ws_ctx, **kwargs)
class StreamingHTTPResponse(BaseHTTPResponse): __slots__ = ( "protocol", "streaming_fn", "status", "content_type", "headers", "_cookies", ) def __init__( self, streaming_fn, status=200, headers=None, content_type="text/plain" ): self.content_type = content_type self.streaming_fn = streaming_fn self.status = status self.headers = CIMultiDict(headers or {}) self._cookies = None async def write(self, data): """Writes a chunk of data to the streaming response. :param data: bytes-ish data to be written. """ if type(data) != bytes: data = self._encode_body(data) self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data)) await self.protocol.drain() async def stream( self, version="1.1", keep_alive=False, keep_alive_timeout=None ): """Streams headers, runs the `streaming_fn` callback that writes content to the response body, then finalizes the response body. """ headers = self.get_headers( version, keep_alive=keep_alive, keep_alive_timeout=keep_alive_timeout, ) self.protocol.push_data(headers) await self.protocol.drain() await self.streaming_fn(self) self.protocol.push_data(b"0\r\n\r\n") # no need to await drain here after this write, because it is the # very last thing we write and nothing needs to wait for it. def get_headers( self, version="1.1", keep_alive=False, keep_alive_timeout=None ): # This is all returned in a kind-of funky way # We tried to make this as fast as possible in pure python timeout_header = b"" if keep_alive and keep_alive_timeout is not None: timeout_header = b"Keep-Alive: %d\r\n" % keep_alive_timeout self.headers["Transfer-Encoding"] = "chunked" self.headers.pop("Content-Length", None) self.headers["Content-Type"] = self.headers.get( "Content-Type", self.content_type ) headers = self._parse_headers() if self.status is 200: status = b"OK" else: status = STATUS_CODES.get(self.status) return (b"HTTP/%b %d %b\r\n" b"%b" b"%b\r\n") % ( version.encode(), self.status, status, timeout_header, headers, )
class Class384(HeadersMixin): var4516 = True def __init__(self, *, status=200, reason=None, headers=None): self.attribute1089 = None self.attribute615 = None self.attribute599 = False self.attribute71 = False self.attribute473 = False self.attribute509 = SimpleCookie() self.attribute2211 = None self.attribute2109 = None self.attribute1416 = False self.attribute1074 = 0 if (headers is not None): self.attribute1239 = CIMultiDict(headers) else: self.attribute1239 = CIMultiDict() self.function2746(function2761, function286) @property def function376(self): return (self.attribute2109 is not None) @property def function1121(self): return getattr(self.attribute2211, 'task', None) @property def function2761(self): return self.attribute1168 @property def function972(self): return self.attribute599 @property def function955(self): return self.attribute71 @property def function286(self): return self.attribute892 def function2746(self, function2761, function286=None, arg1746=RESPONSES): assert (not self.function376), 'Cannot change the response status code after the headers have been sent' self.attribute1168 = int(function2761) if (function286 is None): try: function286 = arg1746[self.attribute1168][0] except: function286 = '' self.attribute892 = function286 @property def function732(self): return self.attribute615 def function94(self): self.attribute615 = False @property def function2122(self): return self.attribute1074 @property def function2738(self): warnings.warn('output_length is deprecated', DeprecationWarning) return self.attribute2109.buffer_size def function469(self, arg782=None): 'Enables automatic chunked transfer encoding.' self.attribute599 = True if (hdrs.CONTENT_LENGTH in self.attribute1239): raise RuntimeError("You can't enable chunked encoding when a content length is set") if (arg782 is not None): warnings.warn('Chunk size is deprecated #1615', DeprecationWarning) def function391(self, arg1099=None): 'Enables response compression encoding.' if (type(arg1099) == bool): arg1099 = (Class387.deflate if arg1099 else Class387.identity) elif (arg1099 is not None): assert isinstance(arg1099, Class387), 'force should one of None, bool or ContentEncoding' self.attribute71 = True self.attribute473 = arg1099 @property def function1363(self): return self.attribute1239 @property def function1741(self): return self.attribute509 def function1565(self, arg268, arg660, *, expires=None, domain=None, max_age=None, path='/', secure=None, httponly=None, version=None): 'Set or update response cookie.\n\n Sets new cookie or updates existent with new value.\n Also updates only those params which are not None.\n ' var578 = self.attribute509.get(arg268) if ((var578 is not None) and (var578.coded_value == '')): self.attribute509.pop(arg268, None) self.attribute509[arg268] = arg660 var4421 = self.attribute509[arg268] if (expires is not None): var4421['expires'] = expires elif (var4421.get('expires') == 'Thu, 01 Jan 1970 00:00:00 GMT'): del var4421['expires'] if (domain is not None): var4421['domain'] = domain if (max_age is not None): var4421['max-age'] = max_age elif ('max-age' in var4421): del var4421['max-age'] var4421['path'] = path if (secure is not None): var4421['secure'] = secure if (httponly is not None): var4421['httponly'] = httponly if (version is not None): var4421['version'] = version def function2042(self, arg2177, *, domain=None, path='/'): 'Delete cookie.\n\n Creates new empty expired cookie.\n ' self.attribute509.pop(arg2177, None) self.function1565(arg2177, '', max_age=0, expires='Thu, 01 Jan 1970 00:00:00 GMT', domain=domain, path=path) @property def function2531(self): return super().function2531 @function2531.setter def function2531(self, arg1097): if (arg1097 is not None): arg1097 = int(arg1097) if self.attribute599: raise RuntimeError("You can't set content length when chunked encoding is enable") self.attribute1239[hdrs.CONTENT_LENGTH] = str(arg1097) else: self.attribute1239.pop(hdrs.CONTENT_LENGTH, None) @property def function2781(self): return super().function2781 @function2781.setter def function2781(self, arg1065): self.function2781 self.attribute1867 = str(arg1065) self.function2316() @property def function1214(self): return super().function1214 @function1214.setter def function1214(self, arg2231): var2907 = self.function2781 if (var2907 == 'application/octet-stream'): raise RuntimeError("Setting charset for application/octet-stream doesn't make sense, setup content_type first") if (arg2231 is None): self._content_dict.pop('charset', None) else: self._content_dict['charset'] = str(arg2231).lower() self.function2316() @property def function2795(self, arg9=hdrs.LAST_MODIFIED): 'The value of Last-Modified HTTP header, or None.\n\n This header is represented as a `datetime` object.\n ' var4672 = self.function1363.get(arg9) if (var4672 is not None): var4612 = parsedate(var4672) if (var4612 is not None): return datetime.datetime(*var4612[:6], tzinfo=datetime.timezone.utc) return None @function2795.setter def function2795(self, arg169): if (arg169 is None): self.function1363.pop(hdrs.LAST_MODIFIED, None) elif isinstance(arg169, (int, float)): self.function1363[hdrs.LAST_MODIFIED] = time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(math.ceil(arg169))) elif isinstance(arg169, datetime.datetime): self.function1363[hdrs.LAST_MODIFIED] = time.strftime('%a, %d %b %Y %H:%M:%S GMT', arg169.utctimetuple()) elif isinstance(arg169, str): self.function1363[hdrs.LAST_MODIFIED] = arg169 @property def function2167(self): var4351 = self.attribute2109 assert (var4351 is not None), 'Cannot get tcp_nodelay for not prepared response' return var4351.function2167 def function507(self, arg484): var2628 = self.attribute2109 assert (var2628 is not None), 'Cannot set tcp_nodelay for not prepared response' var2628.function507(arg484) @property def function2216(self): var2198 = self.attribute2109 assert (var2198 is not None), 'Cannot get tcp_cork for not prepared response' return var2198.function2216 def function1889(self, arg2369): var2008 = self.attribute2109 assert (var2008 is not None), 'Cannot set tcp_cork for not prepared response' var2008.function1889(arg2369) def function2316(self, arg1486=hdrs.CONTENT_TYPE): var774 = '; '.join((('%s=%s' % var458) for var458 in self._content_dict.items())) if var774: var467 = ((self.attribute1867 + '; ') + var774) else: var467 = self.attribute1867 self.function1363[arg1486] = var467 def function717(self, arg1182): if (arg1182 != Class387.identity): self.function1363[hdrs.CONTENT_ENCODING] = arg1182.value self.attribute2109.function391(arg1182.value) self.attribute599 = True def function1206(self, arg1020): if self.attribute473: self.function717(self.attribute473) else: var3651 = arg1020.function1363.get(hdrs.ACCEPT_ENCODING, '').lower() for var1064 in Class387: if (var1064.value in var3651): self.function717(var1064) return @asyncio.coroutine def function1423(self, arg411): if self.attribute1416: return if (self.attribute2109 is not None): return self.attribute2109 yield from arg411._prepare_hook(self) return self.function1679(arg411) def function1679(self, arg1171, arg1370=HttpVersion10, arg500=HttpVersion11, arg1070=hdrs.CONNECTION, arg796=hdrs.DATE, arg2304=hdrs.SERVER, arg396=hdrs.CONTENT_TYPE, arg398=hdrs.CONTENT_LENGTH, arg2106=hdrs.SET_COOKIE, arg37=SERVER_SOFTWARE, arg1263=hdrs.TRANSFER_ENCODING): self.attribute2211 = arg1171 function732 = self.attribute615 if (function732 is None): function732 = arg1171.function732 self.attribute615 = function732 var148 = arg1171.var148 var1810 = self.attribute2109 = arg1171._writer function1363 = self.attribute1239 for var2804 in self.attribute509.values(): var1659 = var2804.output(header='')[1:] function1363.add(arg2106, var1659) if self.attribute71: self.function1206(arg1171) if self.attribute599: if (var148 != arg500): raise RuntimeError('Using chunked encoding is forbidden for HTTP/{0.major}.{0.minor}'.format(arg1171.var148)) var1810.enable_chunking() function1363[arg1263] = 'chunked' if (arg398 in function1363): del function1363[arg398] elif self.var4516: var1810.length = self.function2531 if ((var1810.length is None) and (var148 >= arg500)): var1810.enable_chunking() function1363[arg1263] = 'chunked' if (arg398 in function1363): del function1363[arg398] function1363.setdefault(arg396, 'application/octet-stream') function1363.setdefault(arg796, arg1171.time_service.strtime()) function1363.setdefault(arg2304, arg37) if (arg1070 not in function1363): if function732: if (var148 == arg1370): function1363[arg1070] = 'keep-alive' elif (var148 == arg500): function1363[arg1070] = 'close' var2724 = 'HTTP/{}.{} {} {}\r\n'.format(var148[0], var148[1], self.attribute1168, self.attribute892) var1810.write_headers(var2724, function1363) return var1810 def function1033(self, arg2371): assert isinstance(arg2371, (bytes, bytearray, memoryview)), ('data argument must be byte-ish (%r)' % type(arg2371)) if self.attribute1416: raise RuntimeError('Cannot call write() after write_eof()') if (self.attribute2109 is None): raise RuntimeError('Cannot call write() before prepare()') return self.attribute2109.function1033(arg2371) @asyncio.coroutine def function1559(self): assert (not self.attribute1416), 'EOF has already been sent' assert (self.attribute2109 is not None), 'Response has not been started' yield from self.attribute2109.function1559() @asyncio.coroutine def function271(self, arg2099=b''): assert isinstance(arg2099, (bytes, bytearray, memoryview)), ('data argument must be byte-ish (%r)' % type(arg2099)) if self.attribute1416: return assert (self.attribute2109 is not None), 'Response has not been started' yield from self.attribute2109.function271(arg2099) self.attribute1416 = True self.attribute2211 = None self.attribute1074 = self.attribute2109.output_size self.attribute2109 = None def __repr__(self): if self.attribute1416: var4368 = 'eof' elif self.function376: var4368 = '{} {} '.format(self.attribute2211.method, self.attribute2211.path) else: var4368 = 'not prepared' return '<{} {} {}>'.format(self.__class__.__name__, self.function286, var4368)