Request/Response

Request - HTTP request. Response - HTTP response.

Introduction

In computer science, request-response or request-replica is one of the basic methods used by computers to communicate with each other on a network, in which the first computer sends a request for some data and the second one responds to the request. More specifically, it is a messaging pattern in which a requester sends a request message to a responder system, which receives and processes the request, ultimately returning a message in response. This is similar to a telephone call, in which the caller must wait for the recipient to pick up the phone before anything can be discussed.

Request

Request is a request that contains data for interaction between the client and the API: base URL, endpoint, method to use, headers, etc.

class Request:
    """
    This class describes a request.
    """

    def __init__(self, environ: dict, settings: Settings):
        """
        Constructs a new instance.

        :param      environ:  The environ
        :type       environ:  dict
        """
        self.environ: dict = environ
        self.settings: Settings = settings
        self.method: str = self.environ["REQUEST_METHOD"]
        self.path: str = self.environ["PATH_INFO"]
        self.GET: dict = self._build_get_params_dict(self.environ["QUERY_STRING"])
        self.POST: dict = self._build_post_params_dict(self.environ["wsgi.input"].read())
        self.user_agent: str = self.environ["HTTP_USER_AGENT"]
        self.extra: dict = {}

        logger.debug(f"New request created: {self.method} {self.path}")

    def __getattr__(self, item: Any) -> Union[Any, None]:
        """
        Magic method for get attrs (from extra)

        :param      item:  The item
        :type       item:  Any

        :returns:   Item from self.extra or None
        :rtype:     Union[Any, None]
        """
        return self.extra.get(item, None)

    def _build_get_params_dict(self, raw_params: str):
        """
        Builds a get parameters dictionary.

        :param      raw_params:  The raw parameters
        :type       raw_params:  str
        """
        return parse_qs(raw_params)

    def _build_post_params_dict(self, raw_params: bytes):
        """
        Builds a post parameters dictionary.

        :param      raw_params:  The raw parameters
        :type       raw_params:  bytes
        """
        return parse_qs(raw_params.decode())

Request requires the following arguments to create:

  • environ (dictionary) - web environment (generated by gunicorn)

  • settings (pyechonext.config.Settings dataclass object)

Request has the following public attributes:

  • environ (dictionary) - web environment

  • settings (pyechonext.config.Settings dataclass object)

  • method (string) - http method

  • path (string) - path

  • GET (dictionary) - get request parameters

  • POST (dictionary) - post request parameters

  • user_agent (string) - User-Agent

  • extra (dictionary) - additional parameters (for example for middleware)

Request also has the following methods:

  • __getattr__ - magic descriptor method for getting attributes (to get elements from the extra attribute)

  • _build_get_params_dict - private method for parsing get request parameters

  • _build_post_params_dict - private method for parsing post request parameters

Response

Response is a response that contains the data returned by the server, including content, status code, and headers.

import json
from typing import Dict, Iterable, Union, Any, List, Tuple, Optional
from socks import method
from loguru import logger
from pyechonext.request import Request


class Response:
    """
    This dataclass describes a response.
    """

    default_content_type: str = "text/html"
    default_charset: str = "UTF-8"
    unicode_errors: str = "strict"
    default_conditional_response: bool = False
    default_body_encoding: str = "UTF-8"

    def __init__(
        self,
        request: Request,
        use_i18n: bool = False,
        status_code: Optional[int] = 200,
        body: Optional[str] = None,
        headers: Optional[Dict[str, str]] = {},
        content_type: Optional[str] = None,
        charset: Optional[str] = None,
        **kwargs,
    ):
        """
        Constructs a new instance.

        :param      request:       The request
        :type       request:       Request
        :param      use_i18n:      The use i 18 n
        :type       use_i18n:      bool
        :param      status_code:   The status code
        :type       status_code:   int
        :param      body:          The body
        :type       body:          str
        :param      headers:       The headers
        :type       headers:       Dict[str, str]
        :param      content_type:  The content type
        :type       content_type:  str
        :param      charset:       The charset
        :type       charset:       str
        :param      kwargs:        The keywords arguments
        :type       kwargs:        dictionary
        """
        if status_code == 200:
            self.status_code: str = "200 OK"
        else:
            self.status_code: str = str(status_code)

        if content_type is None:
            self.content_type: str = self.default_content_type
        else:
            self.content_type: str = content_type

        if charset is None:
            self.charset: str = self.default_charset
        else:
            self.charset: str = charset

        if body is not None:
            self.body: str = body
        else:
            self.body: str = ""

        self._headerslist: list = headers
        self._added_headers: list = []
        self.request: Request = request
        self.extra: dict = {}

        self.use_i18n: bool = use_i18n
        self.i18n_kwargs = kwargs

        self._update_headers()

    def __getattr__(self, item: Any) -> Union[Any, None]:
        """
        Magic method for get attrs (from extra)

        :param      item:  The item
        :type       item:  Any

        :returns:   Item from self.extra or None
        :rtype:     Union[Any, None]
        """
        return self.extra.get(item, None)

    def _structuring_headers(self, environ):
        headers = {
            "Host": environ["HTTP_HOST"],
            "Accept": environ["HTTP_ACCEPT"],
            "User-Agent": environ["HTTP_USER_AGENT"],
        }

        for name, value in headers.items():
            self._headerslist.append((name, value))

        for header_tuple in self._added_headers:
            self._headerslist.append(header_tuple)

    def _update_headers(self) -> None:
        """
        Sets the headers by environ.

        :param      environ:  The environ
        :type       environ:  dict
        """
        self._headerslist = [
            ("Content-Type", f"{self.content_type}; charset={self.charset}"),
            ("Content-Length", str(len(self.body))),
        ]

    def add_headers(self, headers: List[Tuple[str, str]]):
        """
        Adds new headers.

        :param      headers:  The headers
        :type       headers:  List[Tuple[str, str]]
        """
        for header in headers:
            self._added_headers.append(header)

    def _encode_body(self):
        """
        Encodes a body.
        """
        if self.content_type.split("/")[-1] == "json":
            self.body = str(self.json)

        try:
            self.body = self.body.encode("UTF-8")
        except AttributeError:
            self.body = str(self.body).encode("UTF-8")

    def __call__(self, environ: dict, start_response: method) -> Iterable:
        """
        Makes the Response object callable.

        :param      environ:         The environ
        :type       environ:         dict
        :param      start_response:  The start response
        :type       start_response:  method

        :returns:   response body
        :rtype:     Iterable
        """
        self._encode_body()

        self._update_headers()
        self._structuring_headers(environ)

        logger.debug(
            f"[{environ['REQUEST_METHOD']} {self.status_code}] Run response: {self.content_type}"
        )

        start_response(status=self.status_code, headers=self._headerslist)

        return iter([self.body])

    @property
    def json(self) -> dict:
        """
        Parse request body as JSON.

        :returns:   json body
        :rtype:     dict
        """
        if self.body:
            if self.content_type.split("/")[-1] == "json":
                return json.dumps(self.body)
            else:
                return json.dumps(self.body.decode("UTF-8"))

        return {}

    def __repr__(self):
        """
        Returns a unambiguous string representation of the object (for debug...).

        :returns:   String representation of the object.
        :rtype:     str
        """
        return f"<{self.__class__.__name__} at 0x{abs(id(self)):x} {self.status_code}>"

Response has the following arguments:

  • request (request class object) - request

  • [optional] status_code (integer value) - response status code

  • [optional] body (string) - response body

  • [optional] headers (dictionary) - response headers

  • [optional] content_type (string) - response content type

  • [optional] charset (string) - response encoding

  • [optional] use_i18n (boolean value) - whether to use i18n (default False)

Response has the following attributes:

  • status_code (string) - status code (default “200 OK”)

  • content_type (string) - content type (defaults to default_content_type)

  • charset (string) - encoding (defaults to default_charset)

  • body (string) - body of the answer (defaults to the empty string)

  • _headerslist (list) - private list of response headers

  • _added_headers (list) - private list of added response headers

  • request (request class object) - request

  • extra (dictionary) - additional parameters

Response has the following methods:

  • __getattr__ - magic descriptor method for getting attributes (to get elements from the extra attribute)

  • _structuring_headers - private method for structuring headers from the web environment

  • _update_headers - private method for updating (overwriting) header lists

  • add_headers - public method for adding headers

  • _encode_body - response body encoding

  • __call__ - magic method, makes the Response object callable

  • json - class property for receiving the response body as json