py.lib.aw_web_tools

Yohn Y. 2024-11-09 Parent:2d0e1f161f26

10:74f5377d83ab Go to Latest

py.lib.aw_web_tools/src/aw_web_tools/btle_tools.py

. Исправление опечаток и неточностей

History
awgur@0 1 # coding: utf-8
awgur@0 2 """\
awgur@0 3 Набор инструментов, для более удобного взаимодействия с фреймворком ``bottle``
awgur@0 4 """
awgur@0 5 import bottle
awgur@0 6 import json
awgur@0 7 from typing import Union, List, Optional, Any
awgur@0 8 from hashlib import sha512
awgur@0 9 from dataclasses import is_dataclass, asdict
awgur@0 10
awgur@0 11 from .cookie import Cookie
awgur@0 12
awgur@0 13
awgur@0 14 VARIABLE_PREFIX = 'ru.a0fs.app' # Префикс переменных и иных структур, которые сохраняются во внутренних
awgur@0 15 # структурах ``bottle``
awgur@0 16
awgur@0 17 # Имена заголовков взяты из собственного стандарта на конфигурарование nginx. При не совпадении нужно сменить.
awgur@0 18 IP_HEADER = 'X-Real-IP' # Заголовок, в котором реверс-прокси хранит реальный IP клиента.
awgur@0 19 REQ_ID_HEADER = 'X-Request-Id' # Заголовок запроса, в котором может храниться уникальный ID запроса,
awgur@0 20 # выставленного реверс-прокси.
awgur@0 21 CONN_ID_HEADER = 'X-Conn-ID' # Идентификатор текущего соединения.
awgur@0 22
awgur@0 23
awgur@3 24 def get_client_ip(request: bottle.BaseRequest = bottle.request) -> str:
awgur@0 25 """\
awgur@0 26 Получение реального адреса клиента
awgur@0 27 """
awgur@3 28 ip = request.get_header(IP_HEADER)
awgur@0 29 if ip:
awgur@0 30 return ip
awgur@0 31
awgur@0 32 else:
awgur@3 33 return request.remote_addr
awgur@0 34
awgur@0 35
awgur@3 36 def get_session_fingerprint(*add_params, request: bottle.BaseRequest = bottle.request) -> str:
awgur@0 37 """\
awgur@0 38 Конструируем некий отпечаток пользовательской сессии.
awgur@0 39
awgur@0 40 В качестве параметра принимается всё, что должно участвовать в создании отпечатка.
awgur@0 41 Это всё превращается в строки и подмешивается в хэш
awgur@3 42
awgur@3 43 При необходимости задать объект запроса, следует использовать форму ``request=<REQOBJ>``
awgur@0 44 """
awgur@3 45 ua = request.get_header('user-agent')
awgur@0 46 add_params_mixin = ':'.join(map(str, add_params))
awgur@0 47
awgur@0 48 return sha512(f'{ua}:{get_client_ip()}:{add_params_mixin}'.encode('utf-8')).hexdigest().lower()
awgur@0 49
awgur@0 50
awgur@0 51 def variable_prep(value, new_value_type: type = str, postprocess: bool = True) -> Any:
awgur@0 52 """\
awgur@0 53 Более интеллектуальная процедура преобразования базовых типов
awgur@0 54
awgur@0 55 :param value: Значение, которое необходимо преобразовать
awgur@0 56 :param new_value_type: Тип, в который необходимо преобразовать значение
awgur@0 57 :param postprocess: Применять ли последующую обработку полученного результата
awgur@0 58 :returns: Значение ``value`` преобразованное к типу ``value_type``
awgur@0 59 """
awgur@0 60 if value is None:
awgur@0 61 return value
awgur@0 62
awgur@0 63 if issubclass(new_value_type, bool):
awgur@0 64 if isinstance(value, str):
awgur@0 65 if value.lower() in ('on', 'yes', '1', 'true',):
awgur@0 66 return True
awgur@0 67 elif value.lower() in ('off', 'no', '0', 'false',):
awgur@0 68 return False
awgur@0 69 else:
awgur@0 70 raise ValueError(f'Unknown method translate "{value}" to "{new_value_type.__name__}"')
awgur@0 71
awgur@0 72 res = new_value_type(value)
awgur@0 73
awgur@0 74 if isinstance(res, str) and postprocess:
awgur@0 75 res = res.strip()
awgur@0 76
awgur@0 77 return res
awgur@0 78
awgur@0 79
awgur@3 80 def get_env(name: str, default: Any = None, request: bottle.BaseRequest = bottle.request):
awgur@0 81 """\
awgur@0 82 Возвращает значения из хранилища контекста запроса bottle
awgur@0 83 """
awgur@0 84 _name = f'{VARIABLE_PREFIX}.{name}'
awgur@3 85 if _name in request.environ:
awgur@3 86 return request.environ[_name]
awgur@0 87
awgur@0 88 else:
awgur@0 89 return default
awgur@0 90
awgur@0 91
awgur@3 92 def set_env(name: str, value: Any, request: bottle.BaseRequest = bottle.request):
awgur@0 93 """\
awgur@0 94 Устанавливает значения в хранилище контекста запроса bottle
awgur@0 95 """
awgur@3 96 request.environ[f'{VARIABLE_PREFIX}.{name}'] = value
awgur@0 97
awgur@0 98
awgur@0 99 def json_type_sanitizer(val):
awgur@0 100 """\
awgur@0 101 Преобразует значение ``val`` в пригодное для преобразования в json значение.
awgur@0 102 """
awgur@0 103 val_t = type(val)
awgur@0 104
awgur@0 105 if is_dataclass(val):
awgur@0 106 return json_type_sanitizer(asdict(val))
awgur@0 107
awgur@0 108 elif val_t in (int, float, str, bool) or val is None:
awgur@0 109 return val
awgur@0 110
awgur@0 111 elif val_t in (list, tuple):
awgur@0 112 return list(map(json_type_sanitizer, val))
awgur@0 113
awgur@0 114 elif val_t == dict:
awgur@0 115 return dict((key, json_type_sanitizer(d_val)) for key, d_val in val.items())
awgur@0 116
awgur@0 117 else:
awgur@0 118 return str(val)
awgur@0 119
awgur@0 120
awgur@0 121 def make_response(
awgur@0 122 data=None, status=200, cookies: Optional[List[Cookie]] = None,
awgur@0 123 remove_cookies: Optional[List[Union[str, Cookie]]] = None
awgur@0 124 ) -> bottle.HTTPResponse:
awgur@0 125 """\
awgur@0 126 Формирует ``API`` ответ.
awgur@0 127 """
awgur@0 128 if data is None:
awgur@0 129 data = {}
awgur@0 130
awgur@0 131 res = bottle.HTTPResponse(body=json.dumps(json_type_sanitizer(data)), status=status)
awgur@0 132 res.content_type = 'application/json'
awgur@0 133
awgur@0 134 if cookies is not None:
awgur@0 135 for cookie in cookies:
awgur@0 136 cookie.response_add(res)
awgur@0 137
awgur@0 138 if remove_cookies is not None and remove_cookies:
awgur@0 139 for cookie in remove_cookies:
awgur@0 140 if isinstance(cookie, Cookie):
awgur@0 141 cookie.response_delete(res)
awgur@0 142
awgur@0 143 else:
awgur@0 144 res.delete_cookie(cookie)
awgur@0 145
awgur@0 146 return res
awgur@0 147
awgur@0 148
awgur@0 149 def get_request_json() -> Optional[dict]:
awgur@0 150 """\
awgur@0 151 Если в запросе, который мы обрабатываем в текущий момент, использовалась посылка данных JSON, получить их.
awgur@0 152 """
awgur@0 153 res = bottle.request.json
awgur@0 154 if res is None:
awgur@0 155 raise ValueError('Не обнаружено JSON в запросе')
awgur@0 156
awgur@0 157 return res
awgur@0 158
awgur@0 159
awgur@3 160 def get_cookie(name: str, request: bottle.BaseRequest = bottle.request) -> Optional[str]:
awgur@0 161 """\
awgur@0 162 Получить значение ``cookie`` с заданным именем из текущего запроса.
awgur@0 163 """
awgur@3 164 return request.get_cookie(name)
awgur@0 165
awgur@0 166
awgur@0 167 def get_param(name: str, param_type: Union[type, str] = str,
awgur@3 168 default=None, postprocess: bool = True,
awgur@3 169 param_source: str = None,
awgur@3 170 request: bottle.BaseRequest = bottle.request
awgur@3 171 ):
awgur@0 172 """\
awgur@0 173 Получить из обрабатываемого на данный момент запроса значения установленного параметра. Обрабатываются как
awgur@0 174 ``GET``, так и ``POST`` параметры, если иное не указано конкретно.
awgur@0 175
awgur@0 176 :param name: Имя параметра
awgur@0 177 :param param_type: Тип, в который нужно преобразовать полученный параметр
awgur@0 178 :param default: Значение, отдаваемое, если параметр не установлен в запросе
awgur@0 179 :param postprocess: Производить ли постобработку параметра
awgur@0 180 :param param_source: Источник параметра, [``get``, ``post``]
awgur@3 181 :param request: Объект обрабатываемого запроса. По умолчанию - ``bottle.request``
awgur@0 182 :returns Полученное из запроса значение
awgur@0 183 """
awgur@0 184 if param_source is None:
awgur@3 185 res = request.params.getunicode(name)
awgur@0 186 elif param_source.lower() == 'get':
awgur@3 187 res = request.GET.getunicode(name)
awgur@0 188 elif param_source.lower() == 'post':
awgur@3 189 res = request.POST.getunicode(name)
awgur@0 190 else:
awgur@0 191 raise ValueError(f'Unknown parameter source "{param_source}" for "{name}"')
awgur@0 192
awgur@0 193 if res is None:
awgur@0 194 res = default
awgur@0 195
awgur@0 196 if isinstance(param_type, str):
awgur@0 197 if param_type.lower() == 'json':
awgur@0 198 if not res:
awgur@0 199 return None
awgur@0 200
awgur@0 201 else:
awgur@0 202 try:
awgur@0 203 return json.loads(res)
awgur@0 204
awgur@0 205 except json.JSONDecodeError as e:
awgur@0 206 raise ValueError(f'Unable to get JSON from from parameter "{name}": param="{res}" error="{e}"')
awgur@0 207 else:
awgur@0 208 try:
awgur@0 209 return variable_prep(res, new_value_type=param_type, postprocess=postprocess)
awgur@0 210
awgur@0 211 except (TypeError, ValueError) as e:
awgur@0 212 raise ValueError(f'Error parsing parameter "{name}": {e}')
awgur@0 213
awgur@0 214
awgur@3 215 def make_log_topic(
awgur@3 216 user: Optional[str] = None,
awgur@3 217 request: bottle.BaseRequest = bottle.request
awgur@3 218 ) -> str:
awgur@0 219 """\
awgur@0 220 Создаём строку, идентифицирующую данный запрос для журналирования операций.
awgur@0 221
awgur@0 222 :param user: Если известен пользователь, задаём его здесь
awgur@3 223 :param request: Объект обрабатываемого запроса. По умолчанию - ``bottle.request``
awgur@0 224 """
awgur@0 225 ip = get_client_ip()
awgur@3 226 url_path = request.fullpath
awgur@3 227 conn_id = request.get_header(CONN_ID_HEADER)
awgur@3 228 req_id = request.get_header(REQ_ID_HEADER)
awgur@0 229
awgur@0 230 if req_id is not None:
awgur@0 231 ip = f'{ip} | {req_id}'
awgur@0 232
awgur@0 233 elif conn_id is not None:
awgur@0 234 ip = f'{ip} | {conn_id}'
awgur@0 235
awgur@0 236 if user is None:
awgur@0 237 ip = f'_NOUID_[{ip}]'
awgur@0 238
awgur@0 239 else:
awgur@0 240 ip = f'{user}[{ip}]'
awgur@0 241
awgur@0 242 return f'{ip} - {url_path}'
awgur@0 243
awgur@0 244
awgur@0 245 def make_error_response(
awgur@0 246 code: int, err: Union[Exception, str],
awgur@0 247 msg: str = None,
awgur@0 248 remove_cookies: Optional[List[Union[str, Cookie]]] = None,
awgur@0 249 cookies: Optional[List[Cookie]] = None
awgur@0 250 ) -> bottle.HTTPResponse:
awgur@0 251 """\
awgur@0 252 Создание сообщения об ошибке для JSON REST API сервисов
awgur@0 253
awgur@0 254 :param code: HTTP-код ответа
awgur@0 255 :param err: Либо объект исключения, либо название ошибки
awgur@0 256 :param msg: Если не ``None`` - сообщение об ошибке. В противном случае, если ``err`` является исключением,
awgur@0 257 сообщение берётся из ``str(err)`` если нет, становится пустой строкой.
awgur@0 258 :param remove_cookies: Список cookie, которые должны быть удалены с клиента.
awgur@0 259 :param cookies: Набор cookies вставляемый в конструктор ответа без изменений. Читать в соответствующем
awgur@0 260 параметре ``make_response``
awgur@0 261 """
awgur@0 262 if isinstance(err, Exception):
awgur@0 263 err_type = type(err).__name__
awgur@0 264 err_msg = str(err) if msg is None else msg
awgur@0 265
awgur@0 266 else:
awgur@0 267 err_type = err
awgur@0 268 err_msg = msg if msg is not None else ''
awgur@0 269
awgur@0 270 res = make_response({
awgur@0 271 'error': err_type,
awgur@0 272 'msg': err_msg,
awgur@0 273 }, code, cookies)
awgur@0 274
awgur@0 275 if remove_cookies is not None and remove_cookies:
awgur@0 276 for cookie in remove_cookies:
awgur@0 277 if isinstance(cookie, Cookie):
awgur@0 278 cookie.response_delete(res)
awgur@0 279
awgur@0 280 else:
awgur@0 281 res.delete_cookie(cookie)
awgur@0 282
awgur@0 283 return res