Финальный проект: ежедневный автоматический отчёт

Пора собрать всё вместе. Спроектируем реальный проект автоматизации — ежедневный отчёт о продажах — от чтения данных до рассылки по расписанию с логами и обработкой ошибок.
Суть: настоящий проект — это пайплайн из функций (читать → считать → оформить → отправить), запускаемый по cron, с логированием и try/except на каждом ответственном шаге.

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

АРХИТЕКТУРА ПРОЕКТА

  cron 09:00
     |
  main()  --try/except + logging--
     |
  read_sales()    -> список сделок (csv)
     |
  build_report()  -> агрегаты по менеджерам
     |
  to_excel()      -> отчёт.xlsx (openpyxl)
     |
  send_email()    -> руководству (smtplib)
     |
  log 'успех/сбой'

Главное в проекте — что main лишь оркестрирует вызовы, а каждая функция делает одну вещь. Такую структуру легко тестировать по частям и чинить. Ядро отчёта — расчёт и проверка — целиком на stdlib, запустим его в браузере как сердце проекта.

Попробуй сам ▶

import logging
from collections import defaultdict

logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')

def read_sales():
    # в реальности — csv.DictReader из файла
    return [('Аня', 75000), ('Боря', 1200), ('Аня', 18000),
            ('Вика', 82000), ('Боря', 3500)]

def build_report(sales):
    if not sales:
        raise ValueError('нет данных за день')
    by_mgr = defaultdict(int)
    for mgr, amount in sales:
        by_mgr[mgr] += amount
    return dict(by_mgr)

def main():
    try:
        sales = read_sales()
        report = build_report(sales)
        total = sum(report.values())
        logging.info(f'Отчёт собран: {len(report)} менеджеров, {total} руб')
        for mgr, amt in sorted(report.items(), key=lambda x: -x[1]):
            print(f'  {mgr:6} {amt:>8,} руб')
        print(f'  {"ИТОГО":6} {total:>8,} руб')
        logging.info('Готово к рассылке')
    except Exception as e:
        logging.error(f'Сбой пайплайна: {e}')

main()

Обратите внимание, как собрались навыки: defaultdict для агрегации, форматирование колонок, logging на каждом шаге, try/except вокруг всего пайплайна и проверка «нет данных». Шаги, трогающие диск и сеть (Excel и email), показаны как врезка.

import logging
from pathlib import Path

def to_excel(report, path):
    from openpyxl import Workbook
    wb = Workbook(); ws = wb.active
    ws.append(['Менеджер', 'Выручка'])
    for mgr, amt in report.items():
        ws.append([mgr, amt])
    wb.save(path)
    return path

def send_email(path):
    import os, smtplib
    from email.message import EmailMessage
    msg = EmailMessage()
    msg['From'] = '[email protected]'; msg['To'] = '[email protected]'
    msg['Subject'] = 'Дневной отчёт'
    msg.set_content('Отчёт во вложении.')
    msg.add_attachment(Path(path).read_bytes(),
        maintype='application',
        subtype='vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        filename='report.xlsx')
    with smtplib.SMTP_SSL('smtp.company.ru', 465) as s:
        s.login('[email protected]', os.environ['EMAIL_PASSWORD'])
        s.send_message(msg)
    logging.info('Отчёт отправлен')

А запускается всё это строкой cron — ежедневно в 9 утра, с логом в файл.

# crontab -e
0 9 * * *  /srv/report/.venv/bin/python /srv/report/main.py >> /srv/report/run.log 2>&1

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

Как работает под капотом

Проект собран по принципу разделения ответственности: каждая функция знает только свою задачу и общается с остальными через возвращаемые данные. main — дирижёр: он вызывает функции по порядку и ловит любые ошибки одним внешним try/except. Если упадёт чтение — не отправится битый отчёт; если упадёт отправка — это попадёт в лог, и вы об этом узнаете.

Запись >> run.log 2>&1 в cron перенаправляет и обычный вывод, и ошибки в файл лога. Так даже падение до инициализации logging (например, синтаксическая ошибка) будет зафиксировано. Это последний рубеж наблюдаемости — без него ночные сбои остаются невидимыми.

Частые ошибки

  • Складывать всё в одну функцию. Монолитный main невозможно тестировать и чинить по частям.
  • Отправлять отчёт до проверки данных. Сначала убедитесь, что данные есть и корректны, потом рассылайте.
  • Забыть про логи cron. Без перенаправления вывода в файл ночное падение не оставит следов.

Best practices

  • Стройте проект как пайплайн из маленьких функций; main только оркестрирует.
  • Тестируйте логику (расчёт) отдельно от побочных эффектов (файлы, сеть).
  • Логируйте каждый этап и перенаправляйте вывод cron в файл — это ваши глаза в проде.

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

Проверьте себя
1. Какова роль функции main в хорошо устроенном проекте автоматизации?
AДелать все операции внутри себя одним куском
BОркестрировать вызовы функций-блоков и ловить ошибки одним try/except
CТолько печатать результат
DЗаменять cron
2. Зачем в cron-строке писать >> run.log 2>&1?
AЧтобы запускать скрипт дважды
BЧтобы перенаправить и обычный вывод, и ошибки в файл лога
CЧтобы ускорить выполнение
DЧтобы скрипт работал в фоне