py.lib.aw_web_tools
2024-02-25
Child:2d0e1f161f26
py.lib.aw_web_tools/src/aw_web_tools/btle_tools.py
..init
1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/src/aw_web_tools/btle_tools.py Sun Feb 25 15:18:36 2024 +0300 1.3 @@ -0,0 +1,273 @@ 1.4 +# coding: utf-8 1.5 +"""\ 1.6 +Набор инструментов, для более удобного взаимодействия с фреймворком ``bottle`` 1.7 +""" 1.8 +import bottle 1.9 +import json 1.10 +from typing import Union, List, Optional, Any 1.11 +from hashlib import sha512 1.12 +from dataclasses import is_dataclass, asdict 1.13 + 1.14 +from .cookie import Cookie 1.15 + 1.16 + 1.17 +VARIABLE_PREFIX = 'ru.a0fs.app' # Префикс переменных и иных структур, которые сохраняются во внутренних 1.18 + # структурах ``bottle`` 1.19 + 1.20 +# Имена заголовков взяты из собственного стандарта на конфигурарование nginx. При не совпадении нужно сменить. 1.21 +IP_HEADER = 'X-Real-IP' # Заголовок, в котором реверс-прокси хранит реальный IP клиента. 1.22 +REQ_ID_HEADER = 'X-Request-Id' # Заголовок запроса, в котором может храниться уникальный ID запроса, 1.23 + # выставленного реверс-прокси. 1.24 +CONN_ID_HEADER = 'X-Conn-ID' # Идентификатор текущего соединения. 1.25 + 1.26 + 1.27 +def get_client_ip() -> str: 1.28 + """\ 1.29 + Получение реального адреса клиента 1.30 + """ 1.31 + ip = bottle.request.get_header(IP_HEADER) 1.32 + if ip: 1.33 + return ip 1.34 + 1.35 + else: 1.36 + return bottle.request.remote_addr 1.37 + 1.38 + 1.39 +def get_session_fingerprint(*add_params) -> str: 1.40 + """\ 1.41 + Конструируем некий отпечаток пользовательской сессии. 1.42 + 1.43 + В качестве параметра принимается всё, что должно участвовать в создании отпечатка. 1.44 + Это всё превращается в строки и подмешивается в хэш 1.45 + """ 1.46 + ua = bottle.request.get_header('user-agent') 1.47 + add_params_mixin = ':'.join(map(str, add_params)) 1.48 + 1.49 + return sha512(f'{ua}:{get_client_ip()}:{add_params_mixin}'.encode('utf-8')).hexdigest().lower() 1.50 + 1.51 + 1.52 +def variable_prep(value, new_value_type: type = str, postprocess: bool = True) -> Any: 1.53 + """\ 1.54 + Более интеллектуальная процедура преобразования базовых типов 1.55 + 1.56 + :param value: Значение, которое необходимо преобразовать 1.57 + :param new_value_type: Тип, в который необходимо преобразовать значение 1.58 + :param postprocess: Применять ли последующую обработку полученного результата 1.59 + :returns: Значение ``value`` преобразованное к типу ``value_type`` 1.60 + """ 1.61 + if value is None: 1.62 + return value 1.63 + 1.64 + if issubclass(new_value_type, bool): 1.65 + if isinstance(value, str): 1.66 + if value.lower() in ('on', 'yes', '1', 'true',): 1.67 + return True 1.68 + elif value.lower() in ('off', 'no', '0', 'false',): 1.69 + return False 1.70 + else: 1.71 + raise ValueError(f'Unknown method translate "{value}" to "{new_value_type.__name__}"') 1.72 + 1.73 + res = new_value_type(value) 1.74 + 1.75 + if isinstance(res, str) and postprocess: 1.76 + res = res.strip() 1.77 + 1.78 + return res 1.79 + 1.80 + 1.81 +def get_env(name: str, default=None): 1.82 + """\ 1.83 + Возвращает значения из хранилища контекста запроса bottle 1.84 + """ 1.85 + _name = f'{VARIABLE_PREFIX}.{name}' 1.86 + if _name in bottle.request.environ: 1.87 + return bottle.request.environ[_name] 1.88 + 1.89 + else: 1.90 + return default 1.91 + 1.92 + 1.93 +def set_env(name: str, value): 1.94 + """\ 1.95 + Устанавливает значения в хранилище контекста запроса bottle 1.96 + """ 1.97 + bottle.request.environ[f'{VARIABLE_PREFIX}.{name}'] = value 1.98 + 1.99 + 1.100 +def json_type_sanitizer(val): 1.101 + """\ 1.102 + Преобразует значение ``val`` в пригодное для преобразования в json значение. 1.103 + """ 1.104 + val_t = type(val) 1.105 + 1.106 + if is_dataclass(val): 1.107 + return json_type_sanitizer(asdict(val)) 1.108 + 1.109 + elif val_t in (int, float, str, bool) or val is None: 1.110 + return val 1.111 + 1.112 + elif val_t in (list, tuple): 1.113 + return list(map(json_type_sanitizer, val)) 1.114 + 1.115 + elif val_t == dict: 1.116 + return dict((key, json_type_sanitizer(d_val)) for key, d_val in val.items()) 1.117 + 1.118 + else: 1.119 + return str(val) 1.120 + 1.121 + 1.122 +def make_response( 1.123 + data=None, status=200, cookies: Optional[List[Cookie]] = None, 1.124 + remove_cookies: Optional[List[Union[str, Cookie]]] = None 1.125 + ) -> bottle.HTTPResponse: 1.126 + """\ 1.127 + Формирует ``API`` ответ. 1.128 + """ 1.129 + if data is None: 1.130 + data = {} 1.131 + 1.132 + res = bottle.HTTPResponse(body=json.dumps(json_type_sanitizer(data)), status=status) 1.133 + res.content_type = 'application/json' 1.134 + 1.135 + if cookies is not None: 1.136 + for cookie in cookies: 1.137 + cookie.response_add(res) 1.138 + 1.139 + if remove_cookies is not None and remove_cookies: 1.140 + for cookie in remove_cookies: 1.141 + if isinstance(cookie, Cookie): 1.142 + cookie.response_delete(res) 1.143 + 1.144 + else: 1.145 + res.delete_cookie(cookie) 1.146 + 1.147 + return res 1.148 + 1.149 + 1.150 +def get_request_json() -> Optional[dict]: 1.151 + """\ 1.152 + Если в запросе, который мы обрабатываем в текущий момент, использовалась посылка данных JSON, получить их. 1.153 + """ 1.154 + res = bottle.request.json 1.155 + if res is None: 1.156 + raise ValueError('Не обнаружено JSON в запросе') 1.157 + 1.158 + return res 1.159 + 1.160 + 1.161 +def get_cookie(name: str) -> Optional[str]: 1.162 + """\ 1.163 + Получить значение ``cookie`` с заданным именем из текущего запроса. 1.164 + """ 1.165 + return bottle.request.get_cookie(name) 1.166 + 1.167 + 1.168 +def get_param(name: str, param_type: Union[type, str] = str, 1.169 + default=None, postprocess: bool = True, param_source: str = None): 1.170 + """\ 1.171 + Получить из обрабатываемого на данный момент запроса значения установленного параметра. Обрабатываются как 1.172 + ``GET``, так и ``POST`` параметры, если иное не указано конкретно. 1.173 + 1.174 + :param name: Имя параметра 1.175 + :param param_type: Тип, в который нужно преобразовать полученный параметр 1.176 + :param default: Значение, отдаваемое, если параметр не установлен в запросе 1.177 + :param postprocess: Производить ли постобработку параметра 1.178 + :param param_source: Источник параметра, [``get``, ``post``] 1.179 + :returns Полученное из запроса значение 1.180 + """ 1.181 + if param_source is None: 1.182 + res = bottle.request.params.getunicode(name) 1.183 + elif param_source.lower() == 'get': 1.184 + res = bottle.request.GET.getunicode(name) 1.185 + elif param_source.lower() == 'post': 1.186 + res = bottle.request.POST.getunicode(name) 1.187 + else: 1.188 + raise ValueError(f'Unknown parameter source "{param_source}" for "{name}"') 1.189 + 1.190 + if res is None: 1.191 + res = default 1.192 + 1.193 + if isinstance(param_type, str): 1.194 + if param_type.lower() == 'json': 1.195 + if not res: 1.196 + return None 1.197 + 1.198 + else: 1.199 + try: 1.200 + return json.loads(res) 1.201 + 1.202 + except json.JSONDecodeError as e: 1.203 + raise ValueError(f'Unable to get JSON from from parameter "{name}": param="{res}" error="{e}"') 1.204 + else: 1.205 + try: 1.206 + return variable_prep(res, new_value_type=param_type, postprocess=postprocess) 1.207 + 1.208 + except (TypeError, ValueError) as e: 1.209 + raise ValueError(f'Error parsing parameter "{name}": {e}') 1.210 + 1.211 + 1.212 +def make_log_topic(user: Optional[str] = None) -> str: 1.213 + """\ 1.214 + Создаём строку, идентифицирующую данный запрос для журналирования операций. 1.215 + 1.216 + :param user: Если известен пользователь, задаём его здесь 1.217 + """ 1.218 + ip = get_client_ip() 1.219 + url_path = bottle.request.fullpath 1.220 + conn_id = bottle.request.get_header(CONN_ID_HEADER) 1.221 + req_id = bottle.request.get_header(REQ_ID_HEADER) 1.222 + 1.223 + if req_id is not None: 1.224 + ip = f'{ip} | {req_id}' 1.225 + 1.226 + elif conn_id is not None: 1.227 + ip = f'{ip} | {conn_id}' 1.228 + 1.229 + if user is None: 1.230 + ip = f'_NOUID_[{ip}]' 1.231 + 1.232 + else: 1.233 + ip = f'{user}[{ip}]' 1.234 + 1.235 + return f'{ip} - {url_path}' 1.236 + 1.237 + 1.238 +def make_error_response( 1.239 + code: int, err: Union[Exception, str], 1.240 + msg: str = None, 1.241 + remove_cookies: Optional[List[Union[str, Cookie]]] = None, 1.242 + cookies: Optional[List[Cookie]] = None 1.243 + ) -> bottle.HTTPResponse: 1.244 + """\ 1.245 + Создание сообщения об ошибке для JSON REST API сервисов 1.246 + 1.247 + :param code: HTTP-код ответа 1.248 + :param err: Либо объект исключения, либо название ошибки 1.249 + :param msg: Если не ``None`` - сообщение об ошибке. В противном случае, если ``err`` является исключением, 1.250 + сообщение берётся из ``str(err)`` если нет, становится пустой строкой. 1.251 + :param remove_cookies: Список cookie, которые должны быть удалены с клиента. 1.252 + :param cookies: Набор cookies вставляемый в конструктор ответа без изменений. Читать в соответствующем 1.253 + параметре ``make_response`` 1.254 + """ 1.255 + if isinstance(err, Exception): 1.256 + err_type = type(err).__name__ 1.257 + err_msg = str(err) if msg is None else msg 1.258 + 1.259 + else: 1.260 + err_type = err 1.261 + err_msg = msg if msg is not None else '' 1.262 + 1.263 + res = make_response({ 1.264 + 'error': err_type, 1.265 + 'msg': err_msg, 1.266 + }, code, cookies) 1.267 + 1.268 + if remove_cookies is not None and remove_cookies: 1.269 + for cookie in remove_cookies: 1.270 + if isinstance(cookie, Cookie): 1.271 + cookie.response_delete(res) 1.272 + 1.273 + else: 1.274 + res.delete_cookie(cookie) 1.275 + 1.276 + return res