py.lib.aw_config
2026-06-23
Parent:260bceaa2d62
py.lib.aw_config/src/aw_config/file.py
.. 0.202606.3 . При формировании имени секции учитываем возможность появления там точек
| awgur@0 | 1 # coding: utf-8 |
| awgur@0 | 2 """\ |
| awgur@0 | 3 Функции, помогающие в разборе конфигурационного файла |
| awgur@0 | 4 """ |
| awgur@0 | 5 import toml |
| awgur@0 | 6 from os.path import exists |
| awgur@0 | 7 from typing import Union, Any |
| awgur@0 | 8 |
| awgur@0 | 9 from .error import Error |
| awgur@0 | 10 from .type_helpers import BaseTypeHelper |
| awgur@7 | 11 from ._lib import _UNDEFINED |
| awgur@0 | 12 |
| awgur@0 | 13 |
| awgur@0 | 14 class ConfigFileError(Error): |
| awgur@0 | 15 """\ |
| awgur@0 | 16 Базовое исключение при обработке конфигурационного файла |
| awgur@0 | 17 """ |
| awgur@0 | 18 |
| awgur@0 | 19 |
| awgur@0 | 20 class ConfigSectionNotFound(ConfigFileError): |
| awgur@0 | 21 """\ |
| awgur@0 | 22 Отсутствие нужного раздела в конфигурации |
| awgur@0 | 23 """ |
| awgur@0 | 24 |
| awgur@0 | 25 |
| awgur@0 | 26 class ConfigFileContent(object): |
| awgur@0 | 27 """\ |
| awgur@0 | 28 Работа с содержимым файла конфигурации |
| awgur@0 | 29 """ |
| awgur@0 | 30 def __init__(self, file_data, section_name=None): |
| awgur@0 | 31 """\ |
| awgur@0 | 32 :param file_data: Разобранное в словарь содержимое файла конфигурации |
| awgur@0 | 33 :param section_name: Имя секции в конфигурации |
| awgur@0 | 34 """ |
| awgur@0 | 35 self.d = file_data |
| awgur@0 | 36 self.section = section_name |
| awgur@0 | 37 |
| awgur@0 | 38 def _format_err_msg(self, msg: str) -> str: |
| awgur@0 | 39 if self.section is not None: |
| awgur@0 | 40 return f'Раздел конфигурации "{self.section}": {msg}' |
| awgur@0 | 41 |
| awgur@0 | 42 else: |
| awgur@0 | 43 return msg |
| awgur@0 | 44 |
| awgur@0 | 45 def __enter__(self): |
| awgur@0 | 46 return self |
| awgur@0 | 47 |
| awgur@0 | 48 def __exit__(self, exc_type, exc_val, exc_tb): |
| awgur@0 | 49 pass |
| awgur@0 | 50 |
| awgur@0 | 51 def get_value(self, name: str, |
| awgur@2 | 52 val_type: Union[type, BaseTypeHelper] = str, |
| awgur@7 | 53 default: Any = _UNDEFINED, |
| awgur@0 | 54 mandatory: bool = True): |
| awgur@0 | 55 """\ |
| awgur@0 | 56 Получить из текущего раздела конфигурации нужный элемент |
| awgur@15 | 57 |
| awgur@0 | 58 :param name: имя параметра |
| awgur@0 | 59 :param val_type: тип параметра |
| awgur@0 | 60 :param default: значение параметра по умолчанию |
| awgur@0 | 61 :param mandatory: является ли параметр обязательным. Если ``False``, то в случае отсутствия |
| awgur@0 | 62 параметра будет использовано значение None |
| awgur@0 | 63 """ |
| awgur@0 | 64 if name not in self.d: |
| awgur@12 | 65 if default is not _UNDEFINED: |
| awgur@12 | 66 return default |
| awgur@12 | 67 |
| awgur@12 | 68 elif mandatory: |
| awgur@0 | 69 raise ConfigFileError(self._format_err_msg(f'Параметр "{name}" в файле отсутствует')) |
| awgur@0 | 70 |
| awgur@0 | 71 else: |
| awgur@12 | 72 return None |
| awgur@0 | 73 |
| awgur@0 | 74 else: |
| awgur@0 | 75 _buf = self.d[name] |
| awgur@0 | 76 try: |
| awgur@0 | 77 return val_type(_buf) |
| awgur@0 | 78 |
| awgur@0 | 79 except (ValueError, TypeError) as e: |
| awgur@0 | 80 raise ConfigFileError(self._format_err_msg(f'Не удалось привести значение "{_buf}" к типу ' |
| awgur@0 | 81 f'{repr(val_type)}: {e}')) |
| awgur@0 | 82 |
| awgur@15 | 83 def _get_section_name(self, *names: str) -> str: |
| awgur@15 | 84 """\ |
| awgur@15 | 85 Поучить имя текущего раздели или подраздела по заданному имени |
| awgur@15 | 86 |
| awgur@15 | 87 :type names: str |
| awgur@15 | 88 :param names: Последовательные имена подразделов для получения доступа к глубокой иерархии |
| awgur@15 | 89 :returns: Имя подраздела |
| awgur@15 | 90 """ |
| awgur@15 | 91 res = self.section if self.section is not None else '' |
| awgur@15 | 92 if not names: |
| awgur@15 | 93 return res |
| awgur@15 | 94 |
| awgur@15 | 95 else: |
| awgur@15 | 96 if res: |
| awgur@15 | 97 res += '.' |
| awgur@15 | 98 |
| awgur@17 | 99 return res + '.'.join( |
| awgur@17 | 100 [(name if name.find('.') == -1 else f'"{name}"') for name in names] |
| awgur@17 | 101 ) |
| awgur@15 | 102 |
| awgur@15 | 103 def _get_part_by_path(self, *names: str): |
| awgur@15 | 104 """\ |
| awgur@15 | 105 Получить конфигурационный элемент по заданному пути |
| awgur@15 | 106 """ |
| awgur@15 | 107 if not names: |
| awgur@15 | 108 raise ValueError(f'Не заданно имя запрашиваемого раздела') |
| awgur@15 | 109 |
| awgur@15 | 110 upper_path = names[:-1] |
| awgur@15 | 111 last_path_el = names[-1] |
| awgur@15 | 112 cur_level = self.d |
| awgur@15 | 113 cur_section = self._get_section_name() |
| awgur@15 | 114 |
| awgur@15 | 115 for el in upper_path: |
| awgur@15 | 116 cur_section = el if not cur_section else f'{cur_section}.{el}' |
| awgur@15 | 117 |
| awgur@15 | 118 if el in cur_level: |
| awgur@15 | 119 cur_level = cur_level[el] |
| awgur@15 | 120 |
| awgur@15 | 121 else: |
| awgur@15 | 122 raise ConfigSectionNotFound(f'Раздел "{cur_section}" не найден') |
| awgur@15 | 123 |
| awgur@15 | 124 cur_section = last_path_el if not cur_section else f'{cur_section}.{last_path_el}' |
| awgur@15 | 125 if last_path_el in cur_level: |
| awgur@15 | 126 return cur_level[last_path_el] |
| awgur@15 | 127 |
| awgur@15 | 128 else: |
| awgur@15 | 129 raise ConfigSectionNotFound(f'Раздел "{cur_section}" не найден') |
| awgur@15 | 130 |
| awgur@15 | 131 |
| awgur@0 | 132 def get_section(self, name: str, mandatory: bool = True): |
| awgur@0 | 133 """\ |
| awgur@0 | 134 Получить раздел конфигурационного файла из текущего или корневого раздела |
| awgur@15 | 135 |
| awgur@0 | 136 :param name: имя раздела |
| awgur@0 | 137 :param mandatory: является ли раздел обязательным или он может отсутствовать в конфигурации. Во втором |
| awgur@0 | 138 случае, будет создан пустой словарь и использован в качестве данных раздела. |
| awgur@15 | 139 :returns: Экземпляр того-же класса, только для новой секции |
| awgur@0 | 140 """ |
| awgur@15 | 141 section_name = self._get_section_name(name) |
| awgur@15 | 142 try: |
| awgur@15 | 143 section_val = self._get_part_by_path(name) |
| awgur@15 | 144 |
| awgur@15 | 145 if not isinstance(section_val, dict): |
| awgur@15 | 146 raise ConfigFileError(f'Параметр конфигурации "{section_name}" не является разделом') |
| awgur@15 | 147 |
| awgur@15 | 148 else: |
| awgur@15 | 149 return ConfigFileContent(file_data=section_val, section_name=f'{section_name}') |
| awgur@15 | 150 |
| awgur@15 | 151 except ConfigSectionNotFound as e: |
| awgur@0 | 152 if mandatory: |
| awgur@15 | 153 raise e |
| awgur@15 | 154 |
| awgur@15 | 155 else: |
| awgur@15 | 156 return ConfigFileContent(file_data={}, section_name=f'{section_name}') |
| awgur@15 | 157 |
| awgur@15 | 158 def get_section_by_path(self, *names: str, mandatory: bool = True): |
| awgur@15 | 159 """\ |
| awgur@15 | 160 Поучить секцию конфигурации по явно заданному пути |
| awgur@15 | 161 |
| awgur@15 | 162 :type names: str |
| awgur@15 | 163 :param names: Имя секции на текущем уровне. Можно задавать несколько |
| awgur@15 | 164 для прохода по всей иерархии необходимой иерархии одним вызовом. |
| awgur@15 | 165 |
| awgur@15 | 166 :type mandatory: bool |
| awgur@15 | 167 :param mandatory: Является ли искомая секция обязательной? |
| awgur@15 | 168 |
| awgur@15 | 169 :returns: Экземпляр того-же класса, только для новой секции |
| awgur@15 | 170 """ |
| awgur@15 | 171 section_name = self._get_section_name(*names) |
| awgur@15 | 172 try: |
| awgur@15 | 173 section_val = self._get_part_by_path(*names) |
| awgur@15 | 174 |
| awgur@15 | 175 if not isinstance(section_val, dict): |
| awgur@15 | 176 raise ConfigFileError(f'Параметр конфигурации "{section_name}" не является разделом') |
| awgur@15 | 177 |
| awgur@15 | 178 else: |
| awgur@15 | 179 return ConfigFileContent(file_data=section_val, section_name=f'{section_name}') |
| awgur@15 | 180 |
| awgur@15 | 181 except ConfigSectionNotFound as e: |
| awgur@15 | 182 if mandatory: |
| awgur@15 | 183 raise e |
| awgur@0 | 184 |
| awgur@0 | 185 else: |
| awgur@15 | 186 return ConfigFileContent(file_data={}, section_name=f'{section_name}') |
| awgur@15 | 187 |
| awgur@15 | 188 def get_list_section(self, name: str, mandatory: bool = True): |
| awgur@15 | 189 """\ |
| awgur@15 | 190 Для случая, когда раздел конфигурации задан массивом toml ([[${! SOME_SECTION !}]]) |
| awgur@15 | 191 |
| awgur@15 | 192 :type name: str |
| awgur@15 | 193 :param name: Имя раздела |
| awgur@15 | 194 |
| awgur@15 | 195 :type mandatory: bool |
| awgur@15 | 196 :param mandatory: Данный раздел обязателен и не может отсутствовать |
| awgur@0 | 197 |
| awgur@15 | 198 :returns: Спискол из экземпляров данного класса, нацеленные на элементы массива хэш-таблиц файла toml |
| awgur@15 | 199 """ |
| awgur@15 | 200 section_name = self._get_section_name(name) |
| awgur@15 | 201 try: |
| awgur@15 | 202 section_val = self._get_part_by_path(name) |
| awgur@0 | 203 |
| awgur@15 | 204 if isinstance(section_val, (list, tuple)): |
| awgur@15 | 205 res = [] |
| awgur@15 | 206 for el in section_val: |
| awgur@15 | 207 if isinstance(el, dict): |
| awgur@15 | 208 res.append(ConfigFileContent(file_data=el, section_name=f'{section_name}')) |
| awgur@15 | 209 |
| awgur@15 | 210 else: |
| awgur@15 | 211 raise ConfigFileError(f'Параметр конфигурации "{section_name}"' |
| awgur@15 | 212 f' со значением "{el}"' |
| awgur@15 | 213 f' не является разделом') |
| awgur@15 | 214 return res |
| awgur@15 | 215 |
| awgur@15 | 216 elif isinstance(section_val, dict): |
| awgur@15 | 217 return [ |
| awgur@15 | 218 ConfigFileContent(file_data=section_val, section_name=f'{section_name}') |
| awgur@15 | 219 ] |
| awgur@0 | 220 |
| awgur@0 | 221 else: |
| awgur@15 | 222 raise ConfigFileError(f'Параметр конфигурации "{section_name}" не является разделом') |
| awgur@15 | 223 |
| awgur@15 | 224 except ConfigSectionNotFound as e: |
| awgur@15 | 225 if mandatory: |
| awgur@15 | 226 raise e |
| awgur@15 | 227 |
| awgur@15 | 228 else: |
| awgur@15 | 229 return [ |
| awgur@15 | 230 ConfigFileContent(file_data={}, section_name=f'{section_name}') |
| awgur@15 | 231 ] |
| awgur@0 | 232 |
| awgur@0 | 233 |
| awgur@0 | 234 class ConfigFile(object): |
| awgur@0 | 235 """\ |
| awgur@0 | 236 Работа с самим файлом конфигурации |
| awgur@0 | 237 """ |
| awgur@0 | 238 def __init__(self, file_name: str): |
| awgur@0 | 239 if not exists(file_name): |
| awgur@0 | 240 raise ConfigFileError(f'Файл конфигурации "{file_name}" не существует') |
| awgur@0 | 241 |
| awgur@0 | 242 self.file_name = file_name |
| awgur@0 | 243 |
| awgur@0 | 244 def get(self): |
| awgur@0 | 245 """\ |
| awgur@0 | 246 Производим разбор файла конфигурации и создаём корневой раздел конфигурации |
| awgur@0 | 247 """ |
| awgur@3 | 248 try: |
| awgur@3 | 249 return ConfigFileContent(toml.load(self.file_name)) |
| awgur@3 | 250 |
| awgur@3 | 251 except OSError as e: |
| awgur@3 | 252 raise ConfigFileError(f'Ошибка чтения файла "{self.file_name}": {e}') |
| awgur@3 | 253 |
| awgur@3 | 254 except toml.TomlDecodeError as e: |
| awgur@3 | 255 raise ConfigFileError(f'Ошибка в разборе файла конфигурации: {e}') |
| awgur@3 | 256 |
| awgur@3 | 257 except ValueError as e: |
| awgur@3 | 258 raise ConfigFileError(f'Ошибка разбора файла конфигурации: {ConfigFileError.fmt_error(e)}') |
| awgur@0 | 259 |
| awgur@0 | 260 def __exit__(self, exc_type, exc_val, exc_tb): |
| awgur@0 | 261 pass |
| awgur@0 | 262 |
| awgur@0 | 263 def __enter__(self): |
| awgur@0 | 264 return self.get() |