В потоке рабочих задач легко упустить из виду рутинные, но важные действия: регулярные бэкапы, проверки серверов, запланированные встречи или просто необходимость переключить контекст. Полагаться на память нерационально, а использование смартфона часто отвлекает от рабочего процесса.

Существующие планировщики часто избыточны для простых задач или требуют постоянного переключения между окнами. Для разработчика наиболее эффективным решением является создание собственного инструмента, интегрированного непосредственно в рабочую среду.

В этой статье мы разработаем гибкую систему десктопных уведомлений на Python. Вместо блокирующих поток конструкций с time.sleep() мы задействуем библиотеку Schedule для чистого синтаксиса планирования и Plyer для вывода нативных системных уведомлений. Разберем, как создать легковесного фонового бота, который будет выполнять функции персонального ассистента, не нагружая систему.

1. Подготовка окружения

Любой проект, претендующий на стабильность, начинается с изоляции зависимостей. Мы не будем засорять глобальный интерпретатор Python, чтобы избежать конфликтов версий библиотек в будущем.

Для начала создадим директорию проекта и развернем в ней виртуальное окружение (Virtual Environment).

Для Windows:

mkdir task_manager
cd task_manager
python -m venv venv
# Активация
venv\Scripts\activate

Для Linux / macOS:

mkdir task_manager
cd task_manager
python3 -m venv venv
# Активация
source venv/bin/activate

После успешной активации (в терминале появится префикс (venv)), установим необходимые библиотеки. Нам понадобятся всего две:

pip install schedule plyer

Разберем наш инструментарий:

  1. Plyer — это кроссплатформенная прослойка (wrapper) для взаимодействия с API операционной системы. Она абстрагирует сложность вызовов нативных функций Windows, Linux (через DBus) и macOS. Это позволяет нам написать один код для отправки уведомлений, который будет работать везде.

  2. Schedule — легковесная библиотека для планирования задач внутри процесса Python. В отличие от системного cron, она не требует конфигурации внешних файлов и предоставляет понятный синтаксис (Fluent Interface) для настройки интервалов, что делает код читаемым и легким в поддержке.

2. Этап 1: Реализация системы уведомлений

Прежде чем переходить к планированию, необходимо реализовать механизм доставки сообщений пользователю. Библиотека plyer предоставляет фасад notification, который унифицирует вызовы API разных операционных систем.

Чтобы не дублировать код вызова notification.notify каждый раз, когда нам нужно отправить сообщение, мы инкапсулируем эту логику в отдельную функцию. Это соответствует принципу DRY (Don't Repeat Yourself) и упростит дальнейшую поддержку.

Создайте файл main.py и импортируйте необходимые модули:

from plyer import notification

def send_notification(title, message):
    """
    Отправляет системное уведомление.
    
    :param title: Заголовок уведомления (например, тип задачи)
    :param message: Основной текст напоминания
    """
    notification.notify(
        title=title,
        message=message,
        app_name='Task Manager',
        # app_icon='icon.ico',  # Путь к иконке (должен быть абсолютным или в той же папке)
        timeout=10  # Время отображения уведомления в секундах
    )

# Тестовый вызов для проверки работы системы
if __name__ == "__main__":
    send_notification("Проверка системы", "Уведомления настроены и работают корректно.")

Разберем параметры метода notify:

  • title и message: Стандартные строковые поля для заголовка и тела уведомления.

  • app_name: Идентификатор приложения, который будет отображаться в центре уведомлений ОС.

  • timeout: Время в секундах, в течение которого уведомление будет висеть на экране, прежде чем исчезнет в историю.

  • app_icon: Опциональный параметр.

    • Нюанс реализации: Для корректного отображения иконки на Windows требуется файл формата .ico. На Linux чаще используются .png. Если вы решите добавить кастомную иконку, рекомендуется использовать модуль pathlib для указания абсолютного пути к файлу, чтобы избежать ошибок при запуске скрипта из другой директории. В данном примере мы оставили этот параметр закомментированным для обеспечения совместимости "из коробки".

Важное замечание для пользователей Linux:
Для работы plyer на некоторых дистрибутивах (например, Ubuntu) может потребоваться наличие системной библиотеки для уведомлений. Если код не выдает ошибок, но уведомления нет, убедитесь, что у вас установлен пакет libnotify-bin:
sudo apt-get install libnotify-bin

3. Этап 2: Логика планирования (Schedule)

Наивная реализация периодических задач в Python часто сводится к использованию бесконечного цикла while True и функции time.sleep(). Этот подход имеет два критических архитектурных недостатка:

  1. Блокировка потока: Во время выполнения sleep интерпретатор "замирает". Если вы захотите добавить проверку нажатия клавиш или параллельную задачу, это станет нетривиальной проблемой.

  2. Временной дрейф (Time Drift): sleep(60) не гарантирует выполнение ровно через 60 секунд. Время выполнения самого кода тела цикла суммируется с паузой, что на длинной дистанции приводит к значительному смещению графика.

Библиотека Schedule решает эти проблемы, предоставляя механизм отложенного выполнения без блокировки всей программы на длительные интервалы. Она использует паттерн Fluent Interface, делая код декларативным и самодокументируемым.

Добавим планирование в наш файл main.py. Нам также понадобится модуль time для работы бесконечного цикла проверки.

import schedule
import time
# from plyer import notification (уже импортировано ранее)

# ... функция send_notification ...

def daily_report():
    send_notification("Ежедневный отчет", "Не забудь заполнить Jira и отправить отчет.")

def server_check():
    # Здесь могла бы быть логика ping-а сервера
    send_notification("Мониторинг", "Проверка серверов выполнена. Статус: OK.")

# --- Конфигурация расписания ---

# 1. Запуск задачи в конкретное время
schedule.every().day.at("09:30").do(daily_report)

# 2. Запуск с интервалом (например, каждые 2 часа)
schedule.every(2).hours.do(server_check)

# 3. Пример передачи аргументов в функцию уведомления напрямую
# Это позволяет переиспользовать одну функцию для разных задач без создания оберток
schedule.every().friday.at("18:00").do(
    send_notification, 
    title="Конец недели", 
    message="Закоммить изменения и хороших выходных!"
)

Механизм выполнения (Event Loop):

Библиотека schedule не запускает отдельный поток или фоновый процесс магическим образом. Она лишь регистрирует задачи в очереди. Чтобы планировщик сработал, нам необходимо инициировать цикл событий (Event Loop), который будет регулярно проверять: «Не наступило ли время для какой-либо задачи?».

Реализуем этот цикл:

if __name__ == "__main__":
    # Первичное уведомление о старте
    send_notification("Task Manager", "Планировщик запущен.")
    
    while True:
        # Проверяет, есть ли отложенные задачи, которые пора выполнить
        schedule.run_pending()
        # Небольшая пауза, чтобы не нагружать CPU холостым циклом на 100%
        time.sleep(1)

Техническое обоснование time.sleep(1):
В данном контексте sleep(1) используется не для отсчета интервала задачи, а исключительно для разгрузки процессора (CPU throttling). Без этой паузы цикл while True будет потреблять одно ядро процессора целиком, проверяя список задач миллионы раз в секунду, что нерационально. Одной проверки в секунду более чем достаточно для задач нашего типа.

4. Этап 3: Сборка приложения (Integration)

Интегрируем разработанные компоненты в единый исполняемый скрипт. Чтобы код не был абстрактным, реализуем сценарий «Типичный рабочий цикл», который автоматизирует рутину разработчика в течение дня.

Сценарий включает:

  1. 10:00 — Напоминание о Daily Standup митинге.

  2. Каждые 2 часа — Мониторинг состояния локальных сервисов (эмуляция).

  3. 18:00 — Напоминание залогировать время в трекер задач (Jira/Redmine) перед уходом.

Полный код приложения (main.py):

import time
import schedule
from plyer import notification

def send_notification(title, message):
    """
    Универсальная функция отправки уведомлений.
    """
    try:
        notification.notify(
            title=title,
            message=message,
            app_name='My Task Manager',
            timeout=10
        )
    except Exception as e:
        print(f"Ошибка отправки уведомления: {e}")

# --- Функции-обработчики задач ---

def morning_standup():
    send_notification(
        "Daily Meeting", 
        "Подключайся к Zoom. Подготовь статус по задачам."
    )

def check_services():
    # В реальном проекте здесь был бы код: response = requests.get('http://localhost:8000')
    # Мы эмулируем проверку
    print("[LOG] Проверка сервисов выполнена.") 
    send_notification(
        "System Monitor", 
        "Локальные сервисы работают штатно. Очередь задач пуста."
    )

def log_work_time():
    send_notification(
        "Конец рабочего дня", 
        "Не забудь списать часы в трекер и запушить код."
    )

# --- Конфигурация планировщика ---

def run_scheduler():
    # 1. Утренний митинг (фиксированное время)
    schedule.every().day.at("10:00").do(morning_standup)

    # 2. Периодическая проверка (интервал)
    schedule.every(2).hours.do(check_services)

    # 3. Закрытие задач (фиксированное время)
    schedule.every().day.at("18:00").do(log_work_time)

    print("Планировщик запущен. Нажмите Ctrl+C для остановки.")
    
    # Основной цикл событий (Event Loop)
    while True:
        schedule.run_pending()
        time.sleep(1)

if __name__ == "__main__":
    run_scheduler()

Архитектурные особенности сборки:

  1. Обработка исключений: Внутри функции send_notification добавлен блок try-except. Это критически важно для фоновых процессов: если служба уведомлений ОС даст сбой, мы должны залогировать ошибку в консоль, а не обрушить весь скрипт (crash), остановив остальные задачи.

  2. Логирование в stdout: Добавлен print внутри check_services и при запуске. Это позволяет убедиться, что скрипт "жив", если запустить его в консоли для отладки.

  3. Масштабируемость: Конфигурация расписания вынесена в отдельную функцию run_scheduler. Это позволяет в будущем легко добавлять новые правила, не меняя логику основного цикла.

5. Этап 4: Запуск в фоновом режиме (Background Mode)

В текущем виде скрипт имеет существенный недостаток UX: он требует держать открытым окно терминала. Если закрыть консоль, процесс завершится, и планировщик остановится. Для создания полноценного системного сервиса («демона») нам необходимо отвязать скрипт от сессии терминала.

Реализация этого механизма зависит от операционной системы.

Для пользователей Windows: Метод .pyw

Как это работает?

Интерпретатор Python в Windows поставляется в двух вариантах:

  1. python.exe — стандартная версия. При запуске всегда открывает черное окно консоли (именно его вы видите при отладке).

  2. pythonw.exe — версия без окна (Windowless). Она предназначена специально для запуска GUI-приложений и фоновых скриптов. Ошибки и вывод print() в этом режиме никуда не пишутся (подавляются), поэтому скрипт работает «молча».

Инструкция по запуску

Шаг 1. Смена расширения
Вам не нужно переписывать код. Достаточно изменить расширение файла вашего скрипта:

  • Было: main.py

  • Стало: main.pyw

Шаг 2. Запуск
Запустите файл двойным кликом.

  • Визуально: Ничего не произойдет (окно не откроется).

  • Фактически: Windows передаст файл интерпретатору pythonw.exe, и скрипт начнет работу в фоне.

Важно: Мы используем сторонние библиотеки (virtual environment), двойной клик может запустить скрипт через глобальный Python, где этих библиотек нет, и скрипт сразу закроется.
Решение: Кликните правой кнопкой мыши по файлу .pyw → «Открыть с помощью» → Выберите pythonw.exe, находящийся внутри папки вашего виртуального окружения (venv/Scripts/pythonw.exe).

Шаг 3. Проверка работы
Так как окна нет, убедиться, что бот работает, можно через Диспетчер задач (Ctrl+Shift+Esc):

  1. Перейдите на вкладку Подробности (Details).

  2. Найдите в списке процесс pythonw.exe. Если он есть и не исчезает через несколько секунд — скрипт успешно работает.

Настройка автозагрузки

Чтобы скрипт стартовал автоматически при включении компьютера:

  1. Создайте ярлык для файла main.pyw (ПКМ → Создать ярлык).

  2. Нажмите Win + R на клавиатуре.

  3. Введите команду shell:startup и нажмите Enter. Откроется системная папка автозагрузки.

  4. Перетащите созданный ярлык в эту папку.

Теперь ваш планировщик будет запускаться вместе с системой в фоновом режиме. Чтобы остановить его, нужно завершить процесс pythonw.exe через Диспетчер задач.

Для пользователей Linux / macOS: Nohup

В Unix-подобных системах запуск фоновых процессов осуществляется средствами оболочки (Shell). Нам необходимо использовать утилиту nohup (no hang up), которая предотвратит завершение процесса при закрытии терминала, и оператор & для перевода процесса в фон.

Команда для запуска:

nohup python3 main.py > output.log 2>&1 &

Разбор команды:

  • nohup — игнорирует сигнал SIGHUP (сигнал отбоя терминала).

  • > output.log — перенаправляет весь вывод (print и ошибки) в файл output.log.

  • 2>&1 — перенаправляет поток ошибок (stderr) туда же, куда и стандартный вывод (stdout).

  • & — возвращает управление терминалу немедленно (background mode).

Как остановить «невидимый» скрипт?

Поскольку у нас нет окна, которое можно закрыть «крестиком», остановка процесса требует явного завершения.

  • Windows: Откройте Диспетчер задач (Ctrl + Shift + Esc), найдите в списке процессов Python (или pythonw.exe) и выберите «Снять задачу».

  • Linux / macOS:

    1. Найдите ID процесса (PID): ps aux | grep main.py

    2. Завершите его: kill <PID> (например, kill 12345).

8. Домашнее задание

Написание кода по туториалу — это лишь первый шаг. Настоящее понимание приходит в процессе модификации и расширения функционала. Предлагаю вам доработать наш скрипт, превратив его из простого примера в полноценный рабочий инструмент.

Выберите уровень сложности и попробуйте реализовать фичу:

Level 1: Junior (Рандомизация контента)
Хардкодить одни и те же сообщения скучно.

  • Задача: Создайте список мотивирующих фраз или профессиональных советов. Измените функцию уведомления так, чтобы она выбирала случайную фразу из списка при каждом срабатывании.

  • Инструменты: Модуль random и метод random.choice().

Level 2: Middle (Конфигурация через JSON)
Хранить настройки (время срабатывания, тексты) внутри кода (main.py) — плохая практика. Если вы захотите изменить время митинга, придется лезть в исходники и перезапускать процесс.

  • Задача: Вынесите все настройки в внешний файл config.json. Напишите функцию, которая при старте скрипта считывает этот файл и настраивает schedule динамически.

  • Инструменты: Модуль json, работа с файлами (open).

Level 3: Senior (Логирование и Аудит)
Фоновые процессы часто «падают» тихо. Вы не узнаете, что скрипт перестал работать, пока не пропустите важную встречу.

  • Задача: Реализуйте систему логирования. Скрипт должен записывать в файл app.log каждое успешное выполнение задачи и, что важнее, любые ошибки (exceptions), возникшие в процессе. Добавьте ротацию логов, чтобы файл не разросся до гигабайтов.

  • Инструменты: Модуль logging (RotatingFileHandler).

Анонсы новых статей, полезные материалы, а так же если в процессе у вас возникнут сложности, обсудить их или задать вопрос по этой статье можно в моём Telegram-сообществе. Смело заходите, если что-то пойдет не так, — постараемся разобраться вместе.

Уверен, у вас все получится. Вперед, к экспериментам

Комментарии (0)