Beispiel #1
0
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)
Beispiel #2
0
    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,
        )
Beispiel #3
0
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)
Beispiel #4
0
 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()
     )
Beispiel #5
0
    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()
            )
Beispiel #6
0
    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
        )
Beispiel #7
0
    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)
Beispiel #8
0
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()
Beispiel #9
0
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
Beispiel #10
0
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()
Beispiel #11
0
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)
Beispiel #12
0
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
Beispiel #13
0
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)
Beispiel #14
0
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
Beispiel #15
0
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)
Beispiel #17
0
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
Beispiel #18
0
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))
Beispiel #19
0
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)
Beispiel #20
0
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)