Урок 88. Презентация проекта

📁 Блок: Финальный проект ⏱️ Время изучения: ~180 мин 🎯 Сложность: Продвинутая
#from #as #and #min #max #count #import #def #return #with #close #try #finally #list

⚡ Кратко: решение

Эталонный скелет проекта — не готовое копирование, а архитектура, которую можно адаптировать.

  • Модули отвечают каждый за свою зону: UI, MySQL, логи, статистика, форматирование.
  • Каждая функция маленькая и решает одну задачу.
  • Обработка ошибок изолирует сбои баз данных от основного цикла.

Что запомнить: хороший скелет проекта — это уже 50% успеха.

Частая ошибка: копировать решение без понимания, почему код устроен именно так.

✅ Эталонный скелет проекта

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

Архитектура

movie_search/
├── main.py              # UI: меню, ввод, вызов других модулей
├── mysql_connector.py   # Работа с MySQL: подключение, поиск, справочники
├── log_writer.py        # Запись логов в MongoDB
├── log_stats.py         # Чтение статистики из MongoDB
└── formatter.py         # Красивый вывод в консоль

main.py — только меню и маршрутизация

def main():
    while True:
        show_menu()
        choice = input("Выбор: ").strip()
        if choice == "1":
            handle_keyword_search()
        elif choice == "2":
            handle_genre_year_search()
        elif choice == "3":
            handle_popular_stats()
        elif choice == "4":
            handle_latest_stats()
        elif choice == "0":
            print("До свидания!")
            break
        else:
            print("Неверный выбор.")

Здесь нет прямого SQL и MongoDB — только вызовы функций из других модулей.

mysql_connector.py — изоляция базы данных

def search_by_keyword(keyword, limit=10, offset=0):
    """Возвращает список фильмов, название которых содержит keyword."""
    ...

def search_by_genre_year(genre, year_from, year_to, limit=10, offset=0):
    """Возвращает фильмы жанра genre, выпущенные между годами."""
    ...

def get_genres():
    """Возвращает отсортированный список жанров."""
    ...

def get_year_range():
    """Возвращает (min_year, max_year) из таблицы film."""
    ...

log_writer.py — атомарная запись

def write_log(collection, search_type, params, results_count):
    """Записывает один поисковый запрос в MongoDB."""
    doc = {
        "timestamp": datetime.now().isoformat(),
        "search_type": search_type,
        "params": params,
        "results_count": results_count,
    }
    collection.insert_one(doc)

log_stats.py — две агрегации

def most_popular(collection, limit=5):
    pipeline = [
        {"$group": {"_id": {"type": "$search_type", "params": "$params"}, "count": {"$sum": 1}}},
        {"$sort": {"count": -1}},
        {"$limit": limit},
    ]
    return list(collection.aggregate(pipeline))

def latest_unique(collection, limit=5):
    pipeline = [
        {"$sort": {"timestamp": -1}},
        {"$group": {"_id": {"type": "$search_type", "params": "$params"}, "timestamp": {"$first": "$timestamp"}}},
        {"$sort": {"timestamp": -1}},
        {"$limit": limit},
    ]
    return list(collection.aggregate(pipeline))

formatter.py — единый формат вывода

def print_films(films):
    if not films:
        print("Ничего не найдено.")
        return
    for film in films:
        print(f"{film['film_id']}: {film['title']} ({film['release_year']})")
        print(f"   {film.get('description', 'Описание отсутствует')}")
    print(f"Всего показано: {len(films)}")

Ключевые принципы

  • Single Responsibility: каждый модуль отвечает за одну область.
  • DRY: подключение к базам, форматирование и обработка ошибок вынесены в одно место.
  • Безопасность: SQL-запросы параметризованы.
  • Отказоустойчивость: ошибки баз данных не прерывают работу приложения.