✅ Решения заданий

⚠️ Рекомендация: попробуйте решить задания самостоятельно, прежде чем смотреть ответы. Решения — это последняя инстанция, когда собственные попытки исчерпаны.

Решение задания 1. Создание базы данных

import pymysql

config = {
    "host": "ich-edit.edu.itcareerhub.de",
    "user": "ich1",
    "password": "ich1_password_ilovedbs",
}

db_name = "bookstore"

with pymysql.connect(**config) as connection:
    with connection.cursor() as cursor:
        cursor.execute(f"CREATE DATABASE IF NOT EXISTS {db_name}")
        cursor.execute("SHOW DATABASES")
        databases = [row[0] for row in cursor]

        if db_name in databases:
            print(f"Database '{db_name}' created or already exists.")

Логика решения: база создаётся с IF NOT EXISTS, чтобы скрипт можно было запускать повторно. Затем мы явно проверяем наличие базы в списке, чтобы убедиться, что команда отработала.

Решение задания 2. Создание таблиц

import pymysql

config = {
    "host": "ich-edit.edu.itcareerhub.de",
    "user": "ich1",
    "password": "ich1_password_ilovedbs",
}

db_name = "bookstore"

books_query = """
    CREATE TABLE IF NOT EXISTS books (
        id INT AUTO_INCREMENT PRIMARY KEY,
        title VARCHAR(200),
        author VARCHAR(100),
        price DECIMAL(10, 2),
        stock INT CHECK (stock >= 0)
    )
"""

users_query = """
    CREATE TABLE IF NOT EXISTS users (
        id INT AUTO_INCREMENT PRIMARY KEY,
        username VARCHAR(100),
        password VARCHAR(100),
        balance DECIMAL(10, 2) CHECK (balance >= 0)
    )
"""

with pymysql.connect(**config) as connection:
    with connection.cursor() as cursor:
        cursor.execute(f"CREATE DATABASE IF NOT EXISTS {db_name}")
        cursor.execute(f"USE {db_name}")
        cursor.execute(books_query)
        cursor.execute(users_query)

        cursor.execute("SHOW TABLES")
        tables = [row[0] for row in cursor]

        print(f"Tables in '{db_name}':")
        for table in tables:
            print(f"- {table}")

Логика решения: сначала создаём базу, затем переключаемся на неё через USE. Таблицы создаём с ограничениями CHECK, чтобы на уровне БД предотвратить отрицательные значения.

Решение задания 3. Загрузка книг из файла

def load_books_from_file(filename: str, connection) -> None:
    added = 0
    with open(filename, encoding="utf-8") as file:
        with connection.cursor() as cursor:
            cursor.execute(f"USE bookstore")
            for line in file:
                parts = line.strip().split(",")
                if len(parts) != 4:
                    continue
                title, author, price, stock = parts
                price = float(price)
                stock = int(stock)

                cursor.execute(
                    "SELECT id, stock FROM books WHERE title = %s AND author = %s",
                    (title, author),
                )
                result = cursor.fetchone()

                if result:
                    book_id, current_stock = result
                    cursor.execute(
                        "UPDATE books SET stock = %s WHERE id = %s",
                        (current_stock + stock, book_id),
                    )
                else:
                    cursor.execute(
                        "INSERT INTO books (title, author, price, stock) VALUES (%s, %s, %s, %s)",
                        (title, author, price, stock),
                    )
                added += 1
    connection.commit()
    print(f"{added} books loaded.")

Логика решения: функция обрабатывает файл построчно. Для каждой строки проверяет существование книги по составному ключу title + author. Если книга найдена — обновляется складской остаток, иначе — вставляется новая запись. commit() вызывается один раз после завершения цикла.

Решение задания 4. Регистрация клиента

def register_user(connection) -> None:
    username = input("Enter username: ")
    password = input("Enter password: ")
    try:
        balance = float(input("Enter initial balance: "))
    except ValueError:
        print("Invalid balance.")
        return

    with connection.cursor() as cursor:
        cursor.execute(f"USE bookstore")
        cursor.execute("SELECT id FROM users WHERE username = %s", (username,))
        if cursor.fetchone():
            print("Username already exists.")
            return

        cursor.execute(
            "INSERT INTO users (username, password, balance) VALUES (%s, %s, %s)",
            (username, password, balance),
        )
    connection.commit()
    print("Registration successful.")

Логика решения: guard clause сначала проверяет корректность баланса, затем — уникальность логина. Если логин занят, функция завершается до выполнения INSERT. Это делает код плоским и понятным.

Решение задания 5. Вход в аккаунт и подменю

def try_login(connection) -> int | None:
    username = input("Enter username: ")
    password = input("Enter password: ")

    with connection.cursor() as cursor:
        cursor.execute(f"USE bookstore")
        cursor.execute(
            "SELECT id FROM users WHERE username = %s AND password = %s",
            (username, password),
        )
        result = cursor.fetchone()

    if result:
        print("Login successful.")
        return result[0]
    print("Invalid username or password.")
    return None


def user_menu(connection, user_id: int) -> None:
    while True:
        print(f"\n--- User Menu (ID {user_id}) ---")
        print("1. View available books")
        print("0. Logout")
        choice = input("Choose action: ")

        if choice == "1":
            print("Books will be shown here.")
        elif choice == "0":
            break
        else:
            print("Invalid option.")

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

Решение задания 6 (★). Поиск книг по названию

def search_books(connection) -> None:
    keyword = input("Enter keyword: ")
    pattern = f"%{keyword}%"

    with connection.cursor() as cursor:
        cursor.execute(f"USE bookstore")
        cursor.execute(
            "SELECT id, title, author, price, stock FROM books WHERE title LIKE %s",
            (pattern,),
        )
        books = cursor.fetchall()

    if not books:
        print("No books found.")
        return

    for book in books:
        book_id, title, author, price, stock = book
        print(f"{book_id}. {title} by {author}, ${price}, stock: {stock}")

Логика решения: шаблон для LIKE формируется в Python, но сам запрос остаётся параметризованным. Так мы избегаем SQL-инъекций и при этом получаем гибкий поиск по подстроке.

Решение задания 7 (★). Переменные окружения

import os
from dotenv import load_dotenv

load_dotenv()

DB_CONFIG = {
    "host": os.getenv("DB_HOST", "localhost"),
    "user": os.getenv("DB_USER", "root"),
    "password": os.getenv("DB_PASSWORD", ""),
}

Пример файла .env:

DB_HOST=ich-edit.edu.itcareerhub.de
DB_USER=ich1
DB_PASSWORD=ich1_password_ilovedbs

Логика решения: секреты загружаются из окружения, а код остаётся безопасным для публикации. Файл .env добавляется в .gitignore, чтобы случайно не попасть в репозиторий.