chutils 2.2.0__tar.gz → 2.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chutils
3
- Version: 2.2.0
3
+ Version: 2.2.1
4
4
  Summary: Набор простых и удобных утилит для Python, который избавляет от рутины при работе с конфигурацией и логированием в новых проектах.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Programming Language :: Python :: 3.13
17
17
  Classifier: Programming Language :: Python :: 3.14
18
18
  Requires-Dist: dotenv (>=0.9.9,<0.10.0)
19
- Requires-Dist: keyring (>=25.6.0,<26.0.0)
19
+ Requires-Dist: keyring (>=25.7.0,<26.0.0)
20
20
  Requires-Dist: python-dotenv (>=1.2.1,<2.0.0)
21
21
  Requires-Dist: pyyaml (>=6.0.3,<7.0.0)
22
22
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "chutils"
3
- version = "2.2.0"
3
+ version = "2.2.1"
4
4
  description = "Набор простых и удобных утилит для Python, который избавляет от рутины при работе с конфигурацией и логированием в новых проектах."
5
5
  authors = ["Chu4hel <sergeiivanov636@gmail.com>"]
6
6
  license = "MIT"
@@ -20,11 +20,14 @@ exclude = [
20
20
  ".ruff_cache/",
21
21
  "coverage.xml",
22
22
  ".coverage",
23
+ "PUBLISHING.md",
24
+ "project.txt",
25
+ "changelog.txt",
23
26
  ]
24
27
 
25
28
  [tool.poetry.dependencies]
26
29
  python = ">=3.9"
27
- keyring = "^25.6.0"
30
+ keyring = "^25.7.0"
28
31
  pyyaml = "^6.0.3"
29
32
  python-dotenv = "^1.2.1"
30
33
  dotenv = "^0.9.9"
@@ -214,8 +214,6 @@ logging.setLoggerClass(ChutilsLogger)
214
214
 
215
215
  # Кэш для пути к директории логов. Изначально пуст.
216
216
  _LOG_DIR: Optional[str] = None
217
- # Глобальный экземпляр основного логгера приложения
218
- _logger_instance: Optional[ChutilsLogger] = None
219
217
  # Флаг, чтобы сообщение об инициализации выводилось только один раз
220
218
  _initialization_message_shown = False
221
219
 
@@ -282,82 +280,72 @@ def setup_logger(
282
280
  backup_count: int = 3
283
281
  ) -> ChutilsLogger:
284
282
  """
285
- Настраивает и возвращает логгер с нужным именем.
283
+ Настраивает и возвращает логгер, всегда обновляя его уровень.
286
284
 
287
- Функция идемпотентна: она предотвращает повторную настройку уже
288
- существующего логгера. Настройки (уровень, имя файла и т.д.) читаются
289
- из конфигурационного файла. По умолчанию добавляются обработчики для
290
- вывода в консоль и в файл с ежедневной ротацией.
285
+ При первом вызове для логгера с указанным именем она настраивает его
286
+ обработчики (для консоли и файла). При последующих вызовах она только
287
+ обновляет уровень логирования, не трогая обработчики, если не указан
288
+ `force_reconfigure=True`.
291
289
 
292
290
  Args:
293
- name: Имя логгера. `app_logger` используется для основного логгера
294
- приложения и его экземпляр кэшируется.
295
- log_level: Явное указание уровня логирования. Если не задан,
296
- значение берется из конфигурационного файла, а если и там нет -
297
- используется 'INFO'.
298
- log_file_name: Опциональное имя файла для логирования. Если указано,
299
- логгер будет писать в этот файл. Если не указано, имя файла
300
- берется из конфигурационного файла ('Logging', 'log_file_name').
291
+ name: Имя логгера. `app_logger` используется как стандартное имя.
292
+ log_level: Явное указание уровня логирования (строкой или LogLevel).
293
+ Если не задан, значение берется из конфигурационного файла.
294
+ log_file_name: Имя файла для логирования. Если не указано, имя берется
295
+ из конфигурации ('Logging', 'log_file_name').
301
296
  force_reconfigure: Если True, принудительно удаляет все существующие
302
297
  обработчики и настраивает логгер заново.
303
- rotation_type: Тип ротации логов. Может быть 'time' (по умолчанию, ежедневная)
304
- или 'size' (по размеру файла).
305
- max_bytes: Максимальный размер файла лога в байтах перед ротацией,
306
- если `rotation_type` установлен в 'size'. По умолчанию 0 (без лимита).
307
- compress: Если True, ротированные файлы логов будут сжиматься в формат .gz.
308
- По умолчанию False.
309
- backup_count: Количество хранимых ротированных файлов логов.
310
- Старые файлы будут удаляться. По умолчанию 3.
298
+ rotation_type: Тип ротации: 'time' (ежедневная) или 'size'.
299
+ max_bytes: Максимальный размер файла для ротации по 'size'.
300
+ compress: Сжимать ли ротированные логи в .gz.
301
+ backup_count: Количество хранимых ротированных файлов.
311
302
 
312
303
  Returns:
313
- logging.Logger: Настроенный экземпляр ChutilsLogger.
304
+ Настроенный экземпляр ChutilsLogger.
314
305
  """
315
- global _logger_instance, _initialization_message_shown
316
- logging.debug(
317
- "Вызов setup_logger() для логгера '%s'. log_file_name: %s, force_reconfigure: %s",
318
- name,
319
- log_file_name,
320
- force_reconfigure
321
- )
322
-
323
- # Если логгер с таким именем уже имеет обработчики, значит он настроен.
324
- # Просто возвращаем его, чтобы не дублировать вывод.
325
- existing_logger = logging.getLogger(name)
326
- if existing_logger.hasHandlers() and not force_reconfigure:
327
- logging.debug("Логгер '%s' уже настроен, возвращаем существующий экземпляр.", name)
328
- return existing_logger # type: ignore
306
+ global _initialization_message_shown
307
+ logger = logging.getLogger(name)
308
+
309
+ # --- 1. Определение и установка уровня логирования ---
310
+ # Этот блок выполняется всегда, чтобы уровень можно было изменить в любой момент.
311
+ cfg = config.get_config() # Загружаем конфигурацию
312
+
313
+ log_level_enum: LogLevel
314
+ if isinstance(log_level, str):
315
+ try:
316
+ log_level_enum = LogLevel(log_level.upper())
317
+ except ValueError:
318
+ log_level_enum = LogLevel.INFO
319
+ elif log_level is None:
320
+ level_from_config = config.get_config_value('Logging', 'log_level', 'INFO', cfg)
321
+ try:
322
+ log_level_enum = LogLevel(level_from_config.upper())
323
+ except ValueError:
324
+ log_level_enum = LogLevel.INFO
325
+ else:
326
+ log_level_enum = log_level
327
+
328
+ level_int = getattr(logging, log_level_enum.value, logging.INFO)
329
+ logger.setLevel(level_int)
330
+ logger.propagate = False
331
+ logging.debug("Уровень логирования для '%s' установлен на: %s (%s)", name, log_level_enum.value, level_int)
332
+
333
+ # --- 2. Настройка обработчиков (только при необходимости) ---
334
+ if logger.hasHandlers() and not force_reconfigure:
335
+ logging.debug("Обработчики для логгера '%s' уже настроены. Пропускаем настройку.", name)
336
+ return logger # type: ignore
329
337
 
330
338
  # Если требуется принудительная перенастройка, очищаем старые обработчики
331
339
  if force_reconfigure:
332
340
  logging.debug("Принудительная перенастройка для '%s'. Удаление старых обработчиков...", name)
333
- for handler in existing_logger.handlers[:]:
334
- handler.close() # Закрываем файлы, если они были открыты
335
- existing_logger.removeHandler(handler)
341
+ for handler in logger.handlers[:]:
342
+ handler.close()
343
+ logger.removeHandler(handler)
336
344
 
337
- # Если запрашивается основной логгер приложения и он уже есть в кэше.
338
- if name == 'app_logger' and _logger_instance:
339
- logging.debug("Возвращаем кэшированный основной логгер.")
340
- return _logger_instance
341
-
342
- # Получаем директорию для логов. Это первая точка, где запускается вся магия поиска путей.
345
+ # Получаем директорию для логов.
343
346
  log_dir = _get_log_dir()
344
347
  logging.debug("setup_logger() получил log_dir: %s", log_dir)
345
348
 
346
- # Загружаем конфигурацию для получения настроек логирования.
347
- cfg = config.get_config()
348
-
349
- # Определяем уровень логирования
350
- if log_level is None:
351
- level_from_config = config.get_config_value('Logging', 'log_level', 'INFO', cfg)
352
- try:
353
- log_level = LogLevel(level_from_config.upper())
354
- except ValueError:
355
- log_level = LogLevel.INFO
356
-
357
- level_int = getattr(logging, log_level.value, logging.INFO)
358
- existing_logger.setLevel(level_int)
359
- logging.debug("Уровень логирования для '%s' установлен на: %s (%s)", name, log_level.value, level_int)
360
-
361
349
  # Определяем имя файла лога
362
350
  if log_file_name is None:
363
351
  log_file_name = config.get_config_value('Logging', 'log_file_name', 'app.log', cfg)
@@ -369,45 +357,25 @@ def setup_logger(
369
357
  compress = config.get_config_boolean('Logging', 'compress', compress, cfg)
370
358
  backup_count = config.get_config_int('Logging', 'log_backup_count', 3, cfg)
371
359
 
372
- # Создаем и настраиваем новый экземпляр логгера
373
- logger = existing_logger
374
- logger.setLevel(level_int)
375
360
  formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
376
361
 
377
- # Обработчик для вывода в консоль (StreamHandler)
362
+ # Обработчик для вывода в консоль
378
363
  console_handler = logging.StreamHandler()
379
364
  console_handler.setFormatter(formatter)
380
365
  logger.addHandler(console_handler)
381
366
 
382
- # Обработчик для записи в файл (TimedRotatingFileHandler)
383
- # Добавляем его, только если директория логов была успешно определена.
367
+ # Обработчик для записи в файл
384
368
  if log_dir and log_file_name:
385
- # ЕСЛИ ПУТЬ ПЕРЕДАН ЯВНО И ОН АБСОЛЮТНЫЙ, ИСПОЛЬЗУЕМ ЕГО
386
- # Это нужно для нашего отладочного теста, который работает во временной папке
387
- if Path(log_file_name).is_absolute():
388
- log_file_path = Path(log_file_name)
389
- else:
390
- log_file_path = Path(log_dir) / log_file_name
369
+ log_file_path = Path(log_file_name) if Path(log_file_name).is_absolute() else Path(log_dir) / log_file_name
391
370
  logging.debug("Попытка настроить файловый обработчик для %s в %s", name, log_file_path)
392
371
  try:
393
372
  file_handler: Optional[logging.FileHandler] = None
394
373
  if rotation_type == 'size':
395
374
  handler_class = CompressingRotatingFileHandler if compress else logging.handlers.RotatingFileHandler
396
- file_handler = handler_class(
397
- log_file_path,
398
- maxBytes=max_bytes,
399
- backupCount=backup_count,
400
- encoding='utf-8'
401
- )
375
+ file_handler = handler_class(log_file_path, maxBytes=max_bytes, backupCount=backup_count, encoding='utf-8')
402
376
  else: # 'time'
403
377
  handler_class = CompressingTimedRotatingFileHandler if compress else SafeTimedRotatingFileHandler
404
- file_handler = handler_class(
405
- log_file_path,
406
- when="D",
407
- interval=1,
408
- backupCount=backup_count,
409
- encoding='utf-8'
410
- )
378
+ file_handler = handler_class(log_file_path, when="D", interval=1, backupCount=backup_count, encoding='utf-8')
411
379
 
412
380
  if file_handler:
413
381
  file_handler.setFormatter(formatter)
@@ -416,18 +384,13 @@ def setup_logger(
416
384
  if not _initialization_message_shown:
417
385
  logger.debug(
418
386
  "Логирование настроено. Уровень: %s. Файл: %s, ротация: %s, сжатие: %s.",
419
- log_level.value, log_file_path, rotation_type, compress
387
+ log_level_enum.value, log_file_path, rotation_type, compress
420
388
  )
421
389
  _initialization_message_shown = True
422
390
  except Exception as e:
423
391
  logger.error("Не удалось настроить файловый обработчик логов для %s: %s", log_file_path, e)
424
- else:
425
- if not _initialization_message_shown:
426
- logger.warning("Директория для логов не настроена. Файловое логирование отключено.")
427
- _initialization_message_shown = True
428
-
429
- # Кэшируем основной логгер приложения
430
- if name == 'app_logger':
431
- _logger_instance = logger
392
+ elif not _initialization_message_shown:
393
+ logger.warning("Директория для логов не настроена. Файловое логирование отключено.")
394
+ _initialization_message_shown = True
432
395
 
433
396
  return logger # type: ignore
File without changes
File without changes
File without changes
File without changes