py.lib
31:4186c3b229fa Browse Files
+ Модуль работы с датаклассами и их наполнения из ORM + Утилиты Bottle + Утилиты JWT + Помошник в парсинге конфигурационных файлов
config_parse_helper.py dataclass_utils.py webapp/bottle_urils.py webapp/jwt_util.py webapp/tool/__init__.py webapp/tool/url.py webapp/url.py
1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/config_parse_helper.py Fri Aug 05 23:58:12 2022 +0300 1.3 @@ -0,0 +1,199 @@ 1.4 +# coding: utf-8 1.5 +from configparser import ConfigParser 1.6 +from dataclasses import is_dataclass, fields, Field, MISSING 1.7 +from typing import Iterable, Optional, Dict, Any 1.8 +from threading import RLock 1.9 +from os import getenv 1.10 + 1.11 + 1.12 +class ConfigParseHelperError(Exception): 1.13 + """\ 1.14 + Ошибки в модуле помощника разборщика конфигураций 1.15 + """ 1.16 + 1.17 + 1.18 +class NoSectionNotification(ConfigParseHelperError): 1.19 + """\ 1.20 + Оповещение об отсутствующей секции конфигурации 1.21 + """ 1.22 + 1.23 + 1.24 +class CPHSectionBase: 1.25 + """\ 1.26 + Базовый класс обработки секции конфигурационного файла 1.27 + """ 1.28 + 1.29 + def get(self, config_prop_name: str, dc_prop_name: str): 1.30 + """\ 1.31 + Получить свойство из конфигурационного файла 1.32 + """ 1.33 + raise NotImplemented() 1.34 + 1.35 + def __enter__(self): 1.36 + return self 1.37 + 1.38 + def __exit__(self, exc_type, exc_val, exc_tb): 1.39 + raise NotImplemented() 1.40 + 1.41 + 1.42 +class CPHAbsentSection(CPHSectionBase): 1.43 + """\ 1.44 + Класс создаваемый на отсутствующую секцию конфигурационного файла 1.45 + """ 1.46 + def get(self, config_prop_name: str, dc_prop_name: str): 1.47 + raise NoSectionNotification() 1.48 + 1.49 + def __exit__(self, exc_type, exc_val, exc_tb): 1.50 + if exc_type == NoSectionNotification: 1.51 + return True 1.52 + 1.53 + 1.54 +class CPHParamGetter(CPHSectionBase): 1.55 + def __init__(self, parser_helper_object): 1.56 + self.ph = parser_helper_object 1.57 + self.params = {} 1.58 + 1.59 + def _add_param(self, param_name: str, param_val: Any): 1.60 + """\ 1.61 + Непосредственное добавление полученного параметра со всеми проверками. 1.62 + """ 1.63 + 1.64 + fld = self.ph.fields.get(param_name) 1.65 + if not isinstance(fld, Field): 1.66 + raise ConfigParseHelperError(f'В классе данных отсутствует свойство "{param_name}", ' 1.67 + f'которое мы должны заполнить из параметра конфигурации: {fld}') 1.68 + 1.69 + if param_val is not None: 1.70 + try: 1.71 + res = fld.type(param_val) 1.72 + 1.73 + except (ValueError, TypeError) as e: 1.74 + raise ConfigParseHelperError(f'При приведении параметра к ' 1.75 + f'заданному типу произошла ошибка: ' 1.76 + f'значение="{param_val}" ошибка="{e}"') 1.77 + 1.78 + else: 1.79 + if fld.default is not MISSING: 1.80 + res = fld.default 1.81 + 1.82 + elif fld.default_factory is not MISSING: 1.83 + res = fld.default_factory() 1.84 + 1.85 + else: 1.86 + raise ConfigParseHelperError('В конфигурации не заданна обязательная опция') 1.87 + 1.88 + self.params[param_name] = res 1.89 + 1.90 + def __exit__(self, exc_type, exc_val, exc_tb): 1.91 + if exc_type is None: 1.92 + self.ph.add_params(self.params) 1.93 + 1.94 + def get(self, config_prop_name: str, dc_prop_name: str): 1.95 + raise NoSectionNotification() 1.96 + 1.97 + 1.98 +class CPHSection(CPHParamGetter): 1.99 + """\ 1.100 + Класс производящий разбор конкретной секции конфигурации 1.101 + """ 1.102 + 1.103 + def __init__(self, parser_helper_object, section: str): 1.104 + super().__init__(parser_helper_object) 1.105 + self.section_name = section 1.106 + self.section = parser_helper_object.conf_parser[section] 1.107 + 1.108 + def get(self, config_prop_name: str, dc_prop_name: str): 1.109 + """\ 1.110 + :param config_prop_name: Имя опции в файле конфигурации 1.111 + :param dc_prop_name: Имя свойства в классе данных, хранящем конфигурацию 1.112 + """ 1.113 + try: 1.114 + self._add_param(dc_prop_name, self.section.get(config_prop_name)) 1.115 + 1.116 + except ConfigParseHelperError as e: 1.117 + raise ConfigParseHelperError(f'Ошибка при разборе параметра "{config_prop_name}" ' 1.118 + f'в секции "{self.section_name}": {e}') 1.119 + 1.120 + 1.121 +class CPHEnvParser(CPHParamGetter): 1.122 + """\ 1.123 + Класс для разбора переменных окружения в том же ключе, что и файла конфигурации 1.124 + """ 1.125 + 1.126 + def get(self, env_name: str, dc_prop_name: str): 1.127 + """\ 1.128 + :param env_name: Имя переменной окружения, хранящей опцию 1.129 + :param dc_prop_name: Имя свойства в классе данных, хранящем конфигурацию 1.130 + """ 1.131 + 1.132 + try: 1.133 + self._add_param(dc_prop_name, getenv(env_name)) 1.134 + 1.135 + except ConfigParseHelperError as e: 1.136 + raise ConfigParseHelperError(f'Ошибка при получении значения из переменной окружения "{env_name}": {e}') 1.137 + 1.138 + 1.139 +class ConfigParseHelper: 1.140 + def __init__(self, config_object_class: type, required_sections: Optional[Iterable[str]] = None): 1.141 + """\ 1.142 + :param config_object_class: Dataclass, который мы подготовили как хранилище параметров конфигурации 1.143 + :param required_sections: Перечисление секций конфигурации, которые мы требуем, чтобы были в файле 1.144 + """ 1.145 + 1.146 + if required_sections is not None: 1.147 + self.req_sections = set(required_sections) 1.148 + 1.149 + else: 1.150 + self.req_sections = set() 1.151 + 1.152 + if not is_dataclass(config_object_class): 1.153 + raise ConfigParseHelperError(f'Представленный в качестве объекта конфигурации класс не является ' 1.154 + f'классом данных: {config_object_class.__name__}') 1.155 + 1.156 + self.res_obj = config_object_class 1.157 + self.fields = dict((fld.name, fld) for fld in fields(config_object_class)) 1.158 + self.conf_parser: Optional[ConfigParser] = None 1.159 + self.config_params = {} 1.160 + self.config_params_lock = RLock() 1.161 + 1.162 + def add_params(self, params: Dict[str, Any]): 1.163 + self.config_params_lock.acquire() 1.164 + try: 1.165 + self.config_params.update(params) 1.166 + 1.167 + finally: 1.168 + self.config_params_lock.release() 1.169 + 1.170 + def section(self, section_name: str) -> CPHSectionBase: 1.171 + if self.conf_parser is None: 1.172 + raise ConfigParseHelperError(f'Прежде чем приступать к разбору файла конфигурации стоит его загрузить') 1.173 + 1.174 + if self.conf_parser.has_section(section_name): 1.175 + return CPHSection(self, section_name) 1.176 + 1.177 + else: 1.178 + return CPHAbsentSection() 1.179 + 1.180 + def load(self, filename: str): 1.181 + res = ConfigParser() 1.182 + try: 1.183 + res.read(filename) 1.184 + 1.185 + except (TypeError, IOError, OSError, ValueError) as e: 1.186 + raise ConfigParseHelperError(f'Не удалось загрузить файл конфигурации: файл="{filename}" ' 1.187 + f'ошибка="{e}"') 1.188 + 1.189 + missing_sections = self.req_sections - set(res.sections()) 1.190 + 1.191 + if missing_sections: 1.192 + missing_sections = ', '.join(missing_sections) 1.193 + raise ConfigParseHelperError(f'В конфигурационном файле отсутствуют секции: {missing_sections}') 1.194 + 1.195 + self.conf_parser = res 1.196 + 1.197 + def get_config(self): 1.198 + try: 1.199 + return self.res_obj(**self.config_params) 1.200 + 1.201 + except (ValueError, TypeError) as e: 1.202 + raise ConfigParseHelperError(f'Не удалось инициализировать объект конфигурации: {e}')
2.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 2.2 +++ b/dataclass_utils.py Fri Aug 05 23:58:12 2022 +0300 2.3 @@ -0,0 +1,66 @@ 2.4 +# coding: utf-8 2.5 + 2.6 +from dataclasses import fields, dataclass, is_dataclass 2.7 +from typing import Union, Dict, Any 2.8 + 2.9 + 2.10 +def _dict_has(obj: Dict[str, Any], key: str) -> bool: 2.11 + return key in obj 2.12 + 2.13 + 2.14 +def _dict_get(obj: Dict[str, Any], key: str) -> Any: 2.15 + return obj[key] 2.16 + 2.17 + 2.18 +def _obj_has(obj: object, key: str) -> bool: 2.19 + return hasattr(obj, key) 2.20 + 2.21 + 2.22 +def _obj_get(obj: object, key: str) -> Any: 2.23 + return getattr(obj, key) 2.24 + 2.25 + 2.26 +def dataobj_extract(obj: Union[object, Dict[str, Any]], dataclass_type: type) -> dataclass: 2.27 + """\ 2.28 + Извлекает объект данных из предоставленного объекта, путём получения из него 2.29 + указанных в классе данных аттрибутов и поиска их в данном объекте. 2.30 + """ 2.31 + 2.32 + params = {} 2.33 + 2.34 + if isinstance(obj, dict): 2.35 + _has = _dict_has 2.36 + _get = _dict_get 2.37 + 2.38 + else: 2.39 + _has = _obj_has 2.40 + _get = _obj_get 2.41 + 2.42 + if not is_dataclass(dataclass_type): 2.43 + raise ValueError(f'Не относится к классам данных: {dataclass_type.__name__}') 2.44 + 2.45 + for fld in fields(dataclass_type): 2.46 + if _has(obj, fld.name): 2.47 + val = _get(obj, fld.name) 2.48 + if val is not None and not isinstance(val, fld.type): 2.49 + try: 2.50 + val = fld.type(val) 2.51 + 2.52 + except (ValueError, TypeError) as e: 2.53 + raise ValueError(f'Аттрибут {fld.name} не может быть получен из значения "{val}"' 2.54 + f' с типом {type(val).__name__} поскольку не может быть преобразован в' 2.55 + f' тип {fld.type.__name__}, заданный в классе данных: {e}') 2.56 + 2.57 + params[fld.name] = val 2.58 + 2.59 + try: 2.60 + res = dataclass_type(**params) 2.61 + 2.62 + except (ValueError, TypeError) as e: 2.63 + _params = ', '.join(map(lambda x: f'{x[0]}="{x[1]}"', params.items())) 2.64 + raise ValueError(f'Не удалось получить объект' 2.65 + f' класс {dataclass_type.__name__}' 2.66 + f' из параметров: {_params}' 2.67 + f' ошибка: {e}') 2.68 + 2.69 + return res
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 3.2 +++ b/webapp/bottle_urils.py Fri Aug 05 23:58:12 2022 +0300 3.3 @@ -0,0 +1,325 @@ 3.4 +# coding: utf-8 3.5 +"""\ 3.6 +Набор инструментов, для более удобного взаимодействия с фреймворком ``bottle`` 3.7 +""" 3.8 + 3.9 +import bottle 3.10 +import json 3.11 +from typing import Union, List, Optional, Tuple, Any 3.12 +from datetime import datetime 3.13 +from hashlib import sha512 3.14 +from dataclasses import is_dataclass, asdict 3.15 + 3.16 + 3.17 +VARIABLE_PREFIX = 'ru.a0fs.app' # Префикс переменных и иных структур, которые сохраняются во внутренних 3.18 + # структурах ``bottle`` 3.19 +IP_HEADER = 'X-Real-IP' # Заголовок, в котором реверс-прокси хранит реальный IP клиента. 3.20 +ID_HEADER = 'X-Req-ID' # Заголовок запроса, в котором может храниться уникальный ID запроса, 3.21 + # выставленного реверс-прокси. 3.22 +CONN_ID_HEADER = 'X-Conn-ID' # Идентификатор текущего соединения. 3.23 + 3.24 + 3.25 +class Cookie(object): 3.26 + """\ 3.27 + Класс хранящий ``cookie`` и способный их устанавливать в объекты http-ответов ``bottle`` 3.28 + """ 3.29 + 3.30 + def __init__(self, name: str, value: str, 3.31 + max_age: int = None, expires: Union[int, datetime] = None, path: str = None, 3.32 + secure: bool = True, httponly: bool = True, 3.33 + samesite: bool = False, domain: str = None): 3.34 + """\ 3.35 + :param name: Имя ``cookie`` 3.36 + :param value: Значение ``cookie`` 3.37 + :param max_age: Время жизни ``cookie`` в секундах. 3.38 + :param expires: Значение времени, когда cookie будет удалена, задаётся в виде ``unix timestamp (int)`` или 3.39 + ``datetime`` 3.40 + :param path: Префикс пути поиска ресурса на данном сайте, для которого следует отправлять данное ``cookie`` 3.41 + :param secure: Отправлять ``cookie`` только по шифрованным каналам связи 3.42 + :param httponly: Сделать ``cookie`` не доступной для ``JavaScript`` 3.43 + :param samesite: Не отправлять данную cookie, если запрос пришёл не с того же сайта (анализируется заголовок 3.44 + referer) 3.45 + :param domain: Имя домена в рамках которого выставляется cookie. В современных браузерах может и глючить 3.46 + """ 3.47 + 3.48 + self.name = name 3.49 + self.value = value 3.50 + self.max_age = max_age 3.51 + self.expires = expires 3.52 + self.path = path 3.53 + self.secure = secure 3.54 + self.httponly = httponly 3.55 + self.samesite = samesite 3.56 + self.domain = domain 3.57 + 3.58 + def response_add(self, resp: bottle.BaseResponse): 3.59 + res = { 3.60 + 'name': self.name, 3.61 + 'value': self.value, 3.62 + 'secure': self.secure, 3.63 + 'httponly': self.httponly, 3.64 + } 3.65 + 3.66 + if self.samesite: 3.67 + res['samesite'] = 'strict' 3.68 + 3.69 + for k in ('max_age', 'expires', 'path', 'domain'): 3.70 + if getattr(self, k) is not None: 3.71 + res[k] = getattr(self, k) 3.72 + 3.73 + resp.set_cookie(**res) 3.74 + 3.75 + 3.76 +def get_client_ip() -> str: 3.77 + """\ 3.78 + Получение реального адреса клиента 3.79 + """ 3.80 + 3.81 + ip = bottle.request.get_header(IP_HEADER) 3.82 + if ip: 3.83 + return ip 3.84 + 3.85 + else: 3.86 + return bottle.request.remote_addr 3.87 + 3.88 + 3.89 +def get_session_fingerprint(*add_params) -> str: 3.90 + """\ 3.91 + Конструируем некий отпечаток пользовательской сессии. 3.92 + 3.93 + В качестве параметра принимается всё, что должно участвовать в создании отпечатка. 3.94 + Это всё превращается в строки и подмешивается в хэш 3.95 + """ 3.96 + 3.97 + ua = bottle.request.get_header('user-agent') 3.98 + add_params_mixin = ':'.join(map(str, add_params)) 3.99 + 3.100 + return sha512(f'{ua}:{get_client_ip()}:{add_params_mixin}'.encode('utf-8')).hexdigest().lower() 3.101 + 3.102 + 3.103 +def variable_prep(value, new_value_type: type = str, postprocess: bool = True) -> Any: 3.104 + """\ 3.105 + Более интеллектуальная процедура преобразования базовых типов 3.106 + 3.107 + :param value: Значение, которое необходимо преобразовать 3.108 + :param new_value_type: Тип в который необходимо преобразовать значение 3.109 + :param postprocess: Применять ли последующую обработку полученного результата 3.110 + :returns: Значение ``value`` преобразованное к типу ``value_type`` 3.111 + """ 3.112 + 3.113 + if value is None: 3.114 + return value 3.115 + 3.116 + if issubclass(new_value_type, bool): 3.117 + if isinstance(value, str): 3.118 + if value.lower() in ('on', 'yes', '1', 'true',): 3.119 + return True 3.120 + elif value.lower() in ('off', 'no', '0', 'false',): 3.121 + return False 3.122 + else: 3.123 + raise ValueError(f'Unknown method translate "{value}" to "{new_value_type.__name__}"') 3.124 + 3.125 + res = new_value_type(value) 3.126 + 3.127 + if isinstance(res, str) and postprocess: 3.128 + res = res.strip() 3.129 + 3.130 + return res 3.131 + 3.132 + 3.133 +def get_variable(name: str, default=None): 3.134 + """\ 3.135 + Возвращает значения из хранилища контекста запроса bottle 3.136 + """ 3.137 + 3.138 + _name = f'{VARIABLE_PREFIX}.{name}' 3.139 + if _name in bottle.request.environ: 3.140 + return bottle.request.environ[_name] 3.141 + 3.142 + else: 3.143 + return default 3.144 + 3.145 + 3.146 +def set_variable(name: str, value): 3.147 + """\ 3.148 + Устанавливает значения в хранилище контекста запроса bottle 3.149 + """ 3.150 + 3.151 + bottle.request.environ[f'{VARIABLE_PREFIX}.{name}'] = value 3.152 + 3.153 + 3.154 +def make_response( 3.155 + data=None, status=200, cookies: Optional[List[Cookie]] = None 3.156 +) -> bottle.HTTPResponse: 3.157 + """\ 3.158 + Формирует ``API`` ответ. 3.159 + """ 3.160 + 3.161 + if data is None: 3.162 + data = {} 3.163 + 3.164 + res = bottle.HTTPResponse(body=json.dumps(json_type_sanitizer(data)), status=status) 3.165 + res.content_type = 'application/json' 3.166 + 3.167 + if cookies is not None: 3.168 + for cookie in cookies: 3.169 + cookie.response_add(res) 3.170 + 3.171 + return res 3.172 + 3.173 + 3.174 +def get_request_json() -> Optional[dict]: 3.175 + """\ 3.176 + Если в запросе, который мы обрабатываем в текущий момент, использовалась посылка данных JSON, получить их. 3.177 + """ 3.178 + 3.179 + res = bottle.request.json 3.180 + if res is None: 3.181 + raise ValueError('Не обнаружено JSON в запросе') 3.182 + 3.183 + return res 3.184 + 3.185 + 3.186 +def get_cookie(name: str) -> Optional[str]: 3.187 + """\ 3.188 + Получить значение ``cookie`` с заданным именем из текущего запроса. 3.189 + """ 3.190 + 3.191 + return bottle.request.get_cookie(name) 3.192 + 3.193 + 3.194 +def get_param(name: str, param_type: Union[type, str] = str, 3.195 + default=None, postprocess: bool = True, param_source: str = None): 3.196 + """\ 3.197 + Получить из обрабатываемого на данный момент запроса значения установленного параметра. Обрабатываются как 3.198 + ``GET``, так и ``POST`` параметры, если иное не указано конкретно. 3.199 + 3.200 + :param name: Имя параметра 3.201 + :param param_type: Тип, в который нужно преобразовать полученный параметр 3.202 + :param default: Значение, отдаваемое, если параметр не установлен в запросе 3.203 + :param postprocess: Производить ли постобработку параметра 3.204 + :param param_source: Источник параметра, [``get``, ``post``] 3.205 + :returns Полученное из запроса значение 3.206 + """ 3.207 + 3.208 + if param_source is None: 3.209 + res = bottle.request.params.getunicode(name) 3.210 + elif param_source.lower() == 'get': 3.211 + res = bottle.request.GET.getunicode(name) 3.212 + elif param_source.lower() == 'post': 3.213 + res = bottle.request.POST.getunicode(name) 3.214 + else: 3.215 + raise ValueError(f'Unknown parameter source "{param_source}" for "{name}"') 3.216 + 3.217 + if res is None: 3.218 + res = default 3.219 + 3.220 + if isinstance(param_type, str): 3.221 + if param_type.lower() == 'json': 3.222 + if not res: 3.223 + return None 3.224 + 3.225 + else: 3.226 + try: 3.227 + return json.loads(res) 3.228 + 3.229 + except json.JSONDecodeError as e: 3.230 + raise ValueError(f'Unable to get JSON from from parameter "{name}": param="{res}" error="{e}"') 3.231 + else: 3.232 + try: 3.233 + return variable_prep(res, new_value_type=param_type, postprocess=postprocess) 3.234 + 3.235 + except (TypeError, ValueError) as e: 3.236 + raise ValueError(f'Error parsing parameter "{name}": {e}') 3.237 + 3.238 + 3.239 +def dict_filter(data: dict, need_keys: Union[List[str], Tuple[str]]) -> dict: 3.240 + """\ 3.241 + Создать новый словарь на основе существующего, добавив в него только нужные ключи 3.242 + """ 3.243 + 3.244 + return dict((key, val) for key, val in data.items() if key in need_keys) 3.245 + 3.246 + 3.247 +def json_type_sanitizer(val): 3.248 + """\ 3.249 + Преобразует значение ``val`` в пригодное для преобразования в json значение. 3.250 + """ 3.251 + 3.252 + val_t = type(val) 3.253 + 3.254 + if is_dataclass(val): 3.255 + return json_type_sanitizer(asdict(val)) 3.256 + 3.257 + elif val_t in (int, float, str, bool) or val is None: 3.258 + return val 3.259 + 3.260 + elif val_t in (list, tuple): 3.261 + return list(map(json_type_sanitizer, val)) 3.262 + 3.263 + elif val_t == dict: 3.264 + return dict((key, json_type_sanitizer(d_val)) for key, d_val in val.items()) 3.265 + 3.266 + else: 3.267 + return str(val) 3.268 + 3.269 + 3.270 +def make_log_ident(user: Optional[str] = None) -> str: 3.271 + """\ 3.272 + Создаём строку, идентифицирующую данный запрос для журналирования операций. 3.273 + 3.274 + :param user: Если известен пользователь, задаём его здесь 3.275 + """ 3.276 + 3.277 + ip = get_client_ip() 3.278 + url_path = bottle.request.fullpath 3.279 + conn_id = bottle.request.get_header(CONN_ID_HEADER) 3.280 + 3.281 + if conn_id is not None: 3.282 + ip = f'{ip} | {conn_id}' 3.283 + 3.284 + if user is not None: 3.285 + ip = f'_NOID_[{ip}]' 3.286 + 3.287 + else: 3.288 + ip = f'{user}[{ip}]' 3.289 + 3.290 + return f'{ip} - {url_path}' 3.291 + 3.292 + 3.293 +def make_error_response( 3.294 + code: int, err: Union[Exception, str], 3.295 + msg: str = None, 3.296 + remove_cookies: Optional[List[str]] = None, 3.297 + cookies: Optional[List[Cookie]] = None 3.298 +) -> bottle.HTTPResponse: 3.299 + """\ 3.300 + Создание сообщения об ошибке для JSON REST API сервисов 3.301 + 3.302 + :param code: HTTP-код ответа 3.303 + :param err: Либо объект исключения, либо название ошибки 3.304 + :param msg: Если не ``None`` - сообщение об ошибке. В противном случае, если ``err`` является исключением, 3.305 + сообщение берётся из ``str(err)`` если нет, становится пустой строкой. 3.306 + :param remove_cookies: Список cookie, которые должны быть удалены с клиента. 3.307 + :param cookies: Набор cookies вставляемый в конструктор ответа без изменений. Читать в соответствующем 3.308 + параметре ``make_response`` 3.309 + """ 3.310 + 3.311 + if isinstance(err, Exception): 3.312 + err_type = type(err).__name__ 3.313 + err_msg = str(err) if msg is None else msg 3.314 + 3.315 + else: 3.316 + err_type = err 3.317 + err_msg = msg if msg is not None else '' 3.318 + 3.319 + res = make_response({ 3.320 + 'error': err_type, 3.321 + 'msg': err_msg, 3.322 + }, code, cookies) 3.323 + 3.324 + if remove_cookies is not None: 3.325 + for cookie_name in remove_cookies: 3.326 + res.delete_cookie(cookie_name) 3.327 + 3.328 + return res
4.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 4.2 +++ b/webapp/jwt_util.py Fri Aug 05 23:58:12 2022 +0300 4.3 @@ -0,0 +1,58 @@ 4.4 +# coding: utf-8 4.5 + 4.6 +import jwt 4.7 +from datetime import datetime, timedelta 4.8 +from typing import Optional 4.9 + 4.10 + 4.11 +JWT_HASH_ALGO = 'HS512' 4.12 + 4.13 + 4.14 +class JWTError(Exception): 4.15 + pass 4.16 + 4.17 + 4.18 +class JWTAuthError(JWTError): 4.19 + """\ 4.20 + Провалена проверка токена на допустимость по подписи времени или прочее 4.21 + """ 4.22 + 4.23 + 4.24 +class JWTHelper(object): 4.25 + def __init__(self, key: str): 4.26 + self.key = key 4.27 + 4.28 + def encode(self, data: dict, timeout: Optional[int] = None) -> str: 4.29 + if timeout is not None: 4.30 + data['exp'] = datetime.utcnow() + timedelta(seconds=timeout) 4.31 + 4.32 + return jwt.encode(data, key=self.key, algorithm=JWT_HASH_ALGO) 4.33 + 4.34 + def decode(self, token: str, check_timeout: bool = False) -> dict: 4.35 + opts = { 4.36 + 'algorithms': [JWT_HASH_ALGO, ] 4.37 + } 4.38 + 4.39 + if check_timeout: 4.40 + opts['options'] = {'require': {'exp'}} 4.41 + 4.42 + if token is None: 4.43 + raise JWTAuthError('Ключ отсутствует') 4.44 + else: 4.45 + token = token.encode('utf-8') 4.46 + 4.47 + try: 4.48 + return jwt.decode(jwt=token, key=self.key, **opts) 4.49 + 4.50 + except (jwt.InvalidIssuerError, jwt.InvalidSignatureError, jwt.ExpiredSignatureError) as e: 4.51 + raise JWTAuthError(str(e)) 4.52 + 4.53 + except jwt.PyJWTError as e: 4.54 + raise JWTError(f'{type(e).__name__}: {e}') 4.55 + 4.56 + @classmethod 4.57 + def make_cls_fabric(cls, key: str): 4.58 + def f(): 4.59 + return cls(key=key) 4.60 + 4.61 + return f 4.62 \ No newline at end of file
5.1 --- a/webapp/tool/__init__.py Sun Jul 17 22:26:31 2022 +0300 5.2 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 5.3 @@ -1,4 +0,0 @@ 5.4 -# -*- coding: utf-8 -*- 5.5 -# --- 5.6 -# 5.7 -# --- 5.8 \ No newline at end of file
6.1 --- a/webapp/tool/url.py Sun Jul 17 22:26:31 2022 +0300 6.2 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 6.3 @@ -1,12 +0,0 @@ 6.4 -# -*- coding: utf-8 -*- 6.5 -# --- 6.6 -# Инструменты для работы с URL 6.7 -# --- 6.8 -from base64 import urlsafe_b64encode as _b64e, urlsafe_b64decode as _b64d 6.9 -from urllib.parse import quote as urlquote 6.10 - 6.11 -def encode(buf): 6.12 - return _b64e(buf.encode('UTF-8')).decode('UTF-8') 6.13 - 6.14 -def decode(buf): 6.15 - return _b64d(buf.encode('UTF-8')).decode('UTF-8')
7.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 7.2 +++ b/webapp/url.py Fri Aug 05 23:58:12 2022 +0300 7.3 @@ -0,0 +1,12 @@ 7.4 +# -*- coding: utf-8 -*- 7.5 +# --- 7.6 +# Инструменты для работы с URL 7.7 +# --- 7.8 +from base64 import urlsafe_b64encode as _b64e, urlsafe_b64decode as _b64d 7.9 +from urllib.parse import quote as urlquote 7.10 + 7.11 +def encode(buf): 7.12 + return _b64e(buf.encode('UTF-8')).decode('UTF-8') 7.13 + 7.14 +def decode(buf): 7.15 + return _b64d(buf.encode('UTF-8')).decode('UTF-8')