|
| 1 | +# pyEchoNext / Request/Response |
| 2 | + |
| 3 | +--- |
| 4 | + |
| 5 | +Request - HTTP-запрос. Response - HTTP-ответ. |
| 6 | + |
| 7 | +## Введение |
| 8 | +В информатике запрос-ответ или запрос-реплика - это один из основных методов, используемых компьютерами для связи друг с другом в сети, при котором первый компьютер отправляет запрос на некоторые данные, а второй отвечает на запрос. Более конкретно, это шаблон обмена сообщениями, при котором запрашивающий отправляет сообщение с запросом системе-ответчику, которая получает и обрабатывает запрос, в конечном счете возвращая сообщение в ответ. Это аналогично телефонному звонку, при котором вызывающий абонент должен дождаться, пока получатель возьмет трубку, прежде чем что-либо можно будет обсудить. |
| 9 | + |
| 10 | +## Request |
| 11 | +**Request** — это запрос, который содержит данные для взаимодействия между клиентом и API: базовый URL, конечную точку, используемый метод, заголовки и т. д.. |
| 12 | + |
| 13 | +```python |
| 14 | +class Request: |
| 15 | + """ |
| 16 | + This class describes a request. |
| 17 | + """ |
| 18 | + |
| 19 | + def __init__(self, environ: dict, settings: Settings): |
| 20 | + """ |
| 21 | + Constructs a new instance. |
| 22 | +
|
| 23 | + :param environ: The environ |
| 24 | + :type environ: dict |
| 25 | + """ |
| 26 | + self.environ: dict = environ |
| 27 | + self.settings: Settings = settings |
| 28 | + self.method: str = self.environ["REQUEST_METHOD"] |
| 29 | + self.path: str = self.environ["PATH_INFO"] |
| 30 | + self.GET: dict = self._build_get_params_dict(self.environ["QUERY_STRING"]) |
| 31 | + self.POST: dict = self._build_post_params_dict(self.environ["wsgi.input"].read()) |
| 32 | + self.user_agent: str = self.environ["HTTP_USER_AGENT"] |
| 33 | + self.extra: dict = {} |
| 34 | + |
| 35 | + logger.debug(f"New request created: {self.method} {self.path}") |
| 36 | + |
| 37 | + def __getattr__(self, item: Any) -> Union[Any, None]: |
| 38 | + """ |
| 39 | + Magic method for get attrs (from extra) |
| 40 | +
|
| 41 | + :param item: The item |
| 42 | + :type item: Any |
| 43 | +
|
| 44 | + :returns: Item from self.extra or None |
| 45 | + :rtype: Union[Any, None] |
| 46 | + """ |
| 47 | + return self.extra.get(item, None) |
| 48 | + |
| 49 | + def _build_get_params_dict(self, raw_params: str): |
| 50 | + """ |
| 51 | + Builds a get parameters dictionary. |
| 52 | +
|
| 53 | + :param raw_params: The raw parameters |
| 54 | + :type raw_params: str |
| 55 | + """ |
| 56 | + return parse_qs(raw_params) |
| 57 | + |
| 58 | + def _build_post_params_dict(self, raw_params: bytes): |
| 59 | + """ |
| 60 | + Builds a post parameters dictionary. |
| 61 | +
|
| 62 | + :param raw_params: The raw parameters |
| 63 | + :type raw_params: bytes |
| 64 | + """ |
| 65 | + return parse_qs(raw_params.decode()) |
| 66 | +``` |
| 67 | + |
| 68 | +Request требует следующие аргументы для создания: |
| 69 | + |
| 70 | + + environ (словарь) - веб-окружение (генерируется gunicorn) |
| 71 | + + settings (объект датакласса pyechonext.config.Settings) |
| 72 | + |
| 73 | +Request имеет следующие публичные атрибуты: |
| 74 | + |
| 75 | + + environ (словарь) - веб-окружение |
| 76 | + + settings (объект датакласса pyechonext.config.Settings) |
| 77 | + + method (строка) - http-метод |
| 78 | + + path (строка) - путь |
| 79 | + + GET (словарь) - параметры get-запроса |
| 80 | + + POST (словарь) - параметры post-запроса |
| 81 | + + user_agent (строка) - User-Agent |
| 82 | + + extra (словарь) - дополнительные параметры (например для middleware) |
| 83 | + |
| 84 | +Request также имеет следующие методы: |
| 85 | + |
| 86 | + + `__getattr__` - магический метод дескриптора для получения атрибутов (для получения элементов из атрибута extra) |
| 87 | + + `_build_get_params_dict` - приватный метод для парсинга параметров get-запроса |
| 88 | + + `_build_post_params_dict` - приватный метод для парсинга параметров post-запроса |
| 89 | + |
| 90 | +## Response |
| 91 | +**Response** — это ответ, который содержит данные, возвращаемые сервером, в том числе контент, код состояния и заголовки. |
| 92 | + |
| 93 | +```python |
| 94 | +class Response: |
| 95 | + """ |
| 96 | + This dataclass describes a response. |
| 97 | + """ |
| 98 | + |
| 99 | + default_content_type: str = "application/json" |
| 100 | + default_charset: str = "UTF-8" |
| 101 | + unicode_errors: str = "strict" |
| 102 | + default_conditional_response: bool = False |
| 103 | + default_body_encoding: str = "UTF-8" |
| 104 | + |
| 105 | + def __init__( |
| 106 | + self, |
| 107 | + request: Request, |
| 108 | + status_code: Optional[int] = 200, |
| 109 | + body: Optional[str] = None, |
| 110 | + headers: Optional[Dict[str, str]] = {}, |
| 111 | + content_type: Optional[str] = None, |
| 112 | + charset: Optional[str] = None, |
| 113 | + ): |
| 114 | + """ |
| 115 | + Constructs a new instance. |
| 116 | +
|
| 117 | + :param status_code: The status code |
| 118 | + :type status_code: int |
| 119 | + :param body: The body |
| 120 | + :type body: str |
| 121 | + :param headers: The headers |
| 122 | + :type headers: Dict[str, str] |
| 123 | + :param content_type: The content type |
| 124 | + :type content_type: str |
| 125 | + """ |
| 126 | + if status_code == 200: |
| 127 | + self.status_code: str = "200 OK" |
| 128 | + else: |
| 129 | + self.status_code: str = str(status_code) |
| 130 | + |
| 131 | + if content_type is None: |
| 132 | + self.content_type: str = self.default_content_type |
| 133 | + else: |
| 134 | + self.content_type: str = content_type |
| 135 | + |
| 136 | + if charset is None: |
| 137 | + self.charset: str = self.default_charset |
| 138 | + else: |
| 139 | + self.charset: str = charset |
| 140 | + |
| 141 | + if body is not None: |
| 142 | + self.body: str = body |
| 143 | + else: |
| 144 | + self.body: str = "" |
| 145 | + |
| 146 | + self._headerslist: list = headers |
| 147 | + self._added_headers: list = [] |
| 148 | + self.request: Request = request |
| 149 | + self.extra: dict = {} |
| 150 | + |
| 151 | + self._update_headers() |
| 152 | + |
| 153 | + def __getattr__(self, item: Any) -> Union[Any, None]: |
| 154 | + """ |
| 155 | + Magic method for get attrs (from extra) |
| 156 | +
|
| 157 | + :param item: The item |
| 158 | + :type item: Any |
| 159 | +
|
| 160 | + :returns: Item from self.extra or None |
| 161 | + :rtype: Union[Any, None] |
| 162 | + """ |
| 163 | + return self.extra.get(item, None) |
| 164 | + |
| 165 | + def _structuring_headers(self, environ): |
| 166 | + headers = { |
| 167 | + "Host": environ["HTTP_HOST"], |
| 168 | + "Accept": environ["HTTP_ACCEPT"], |
| 169 | + "User-Agent": environ["HTTP_USER_AGENT"], |
| 170 | + } |
| 171 | + |
| 172 | + for name, value in headers.items(): |
| 173 | + self._headerslist.append((name, value)) |
| 174 | + |
| 175 | + for header_tuple in self._added_headers: |
| 176 | + self._headerslist.append(header_tuple) |
| 177 | + |
| 178 | + def _update_headers(self) -> None: |
| 179 | + """ |
| 180 | + Sets the headers by environ. |
| 181 | +
|
| 182 | + :param environ: The environ |
| 183 | + :type environ: dict |
| 184 | + """ |
| 185 | + self._headerslist = [ |
| 186 | + ("Content-Type", f"{self.content_type}; charset={self.charset}"), |
| 187 | + ("Content-Length", str(len(self.body))), |
| 188 | + ] |
| 189 | + |
| 190 | + def add_headers(self, headers: List[Tuple[str, str]]): |
| 191 | + """ |
| 192 | + Adds new headers. |
| 193 | +
|
| 194 | + :param headers: The headers |
| 195 | + :type headers: List[Tuple[str, str]] |
| 196 | + """ |
| 197 | + for header in headers: |
| 198 | + self._added_headers.append(header) |
| 199 | + |
| 200 | + def _encode_body(self): |
| 201 | + """ |
| 202 | + Encodes a body. |
| 203 | + """ |
| 204 | + if self.content_type.split("/")[-1] == "json": |
| 205 | + self.body = str(self.json) |
| 206 | + |
| 207 | + try: |
| 208 | + self.body = self.body.encode("UTF-8") |
| 209 | + except AttributeError: |
| 210 | + self.body = str(self.body).encode("UTF-8") |
| 211 | + |
| 212 | + def __call__(self, environ: dict, start_response: method) -> Iterable: |
| 213 | + """ |
| 214 | + Makes the Response object callable. |
| 215 | +
|
| 216 | + :param environ: The environ |
| 217 | + :type environ: dict |
| 218 | + :param start_response: The start response |
| 219 | + :type start_response: method |
| 220 | +
|
| 221 | + :returns: response body |
| 222 | + :rtype: Iterable |
| 223 | + """ |
| 224 | + self._encode_body() |
| 225 | + |
| 226 | + self._update_headers() |
| 227 | + self._structuring_headers(environ) |
| 228 | + |
| 229 | + logger.debug( |
| 230 | + f"[{environ['REQUEST_METHOD']} {self.status_code}] Run response: {self.content_type}" |
| 231 | + ) |
| 232 | + |
| 233 | + start_response(status=self.status_code, headers=self._headerslist) |
| 234 | + |
| 235 | + return iter([self.body]) |
| 236 | + |
| 237 | + @property |
| 238 | + def json(self) -> dict: |
| 239 | + """ |
| 240 | + Parse request body as JSON. |
| 241 | +
|
| 242 | + :returns: json body |
| 243 | + :rtype: dict |
| 244 | + """ |
| 245 | + if self.body: |
| 246 | + if self.content_type.split("/")[-1] == "json": |
| 247 | + return json.dumps(self.body) |
| 248 | + else: |
| 249 | + return json.dumps(self.body.decode("UTF-8")) |
| 250 | + |
| 251 | + return {} |
| 252 | + |
| 253 | + def __repr__(self): |
| 254 | + """ |
| 255 | + Returns a unambiguous string representation of the object (for debug...). |
| 256 | +
|
| 257 | + :returns: String representation of the object. |
| 258 | + :rtype: str |
| 259 | + """ |
| 260 | + return f"<{self.__class__.__name__} at 0x{abs(id(self)):x} {self.status_code}>" |
| 261 | +``` |
| 262 | + |
| 263 | +Response имеет следующие аргументы: |
| 264 | + |
| 265 | + + request (объект класса Request) - запрос |
| 266 | + + [опционально] status_code (целочисленное значение) - статус-код ответа |
| 267 | + + [опционально] body (строка) - тело ответа |
| 268 | + + [опционально] headers (словарь) - заголовки ответа |
| 269 | + + [опционально] content_type (строка) - тип контента ответа |
| 270 | + + [опционально] charset (строка) - кодировка ответа |
| 271 | + |
| 272 | +Response имеет следующие атрибуты: |
| 273 | + |
| 274 | + + status_code (строка) - статус-код (по умолчанию "200 OK") |
| 275 | + + content_type (строка) - контент-тип (по умолчанию равен значению default_content_type) |
| 276 | + + charset (строка) - кодировка (по умолчанию равен значению default_charset) |
| 277 | + + body (строка) - тело овтета (по умолчанию равен пустой строке) |
| 278 | + + `_headerslist` (список) - приватный список заголовков ответа |
| 279 | + + `_added_headers` (список) - приватный список добавленных заголовков ответа |
| 280 | + + request (объект класса Request) - запрос |
| 281 | + + extra (словарь) - дополнительные параметры |
| 282 | + |
| 283 | +Response имеет следующие методы: |
| 284 | + |
| 285 | + + `__getattr__` - магический метод дескриптора для получения атрибутов (для получения элементов из атрибута extra) |
| 286 | + + `_structuring_headers` - приватный метод структуирования заголовков из веб-окружения |
| 287 | + + `_update_headers` - приватный метод обновления (перезаписывания) списков заголовков |
| 288 | + + `add_headers` - публичный метод добавления заголовков |
| 289 | + + `_encode_body` - кодирование тела ответа |
| 290 | + + `__call__` - магический метод, делает объект Response вызываемым |
| 291 | + + `json` - свойство класса для получения тела ответа в виде json |
| 292 | + |
| 293 | +--- |
| 294 | + |
| 295 | +[Содержание](./index.md) |
| 296 | + |
0 commit comments