py.lib.aw_web_tools

Yohn Y. 2024-02-25 Child:2d0e1f161f26

0:06f00ec09030 Go to Latest

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

..init

History
     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