/
gr.ev.vl
/
TestGen
Обзор
Документация
Войти
/
gr.ev.vl
/
TestGen
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master2
backend/scripts/seed_data.py
858 строк
41 KB
gr.ev.vl
Initial commit
06 июн 2026, 02:41
06 июн 2026, 02:41
02fb1fb
Код
Авторство
О чём код?
#!/usr/bin/env python3 """ Наполнение базы данных тестовыми данными. Создаёт: - Пользователей (админ, методист, преподаватели) - Курсы с паспортами и таксономией Блума - Темы и подтемы - Предустановленные промты с версиями - Демо-вопросы с дистракторами - Учебные заблуждения Использование: python scripts/seed_data.py python scripts/seed_data.py --env development """ import asyncio import argparse import sys import os from uuid import uuid4 from datetime import datetime, timezone # Добавление src в путь sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from testgen.config import settings from testgen.domain.models.base import Base from testgen.domain.repositories.user import UserRepository from testgen.domain.repositories.course import CourseRepository from testgen.domain.repositories.passport import PassportRepository from testgen.domain.repositories.topic import TopicRepository, SubtopicRepository from testgen.domain.repositories.question import QuestionRepository from testgen.domain.repositories.distractor import DistractorRepository from testgen.domain.repositories.misconception import MisconceptionRepository from testgen.domain.repositories.prompt import PromptRepository from testgen.core.utils.hashing import hash_password from testgen.core.enums import ( CourseStatus, QuestionStatus, BloomLevel, DistractorType, UserRole, PipelineStep, ) def create_session_factory(): """Создаёт фабрику сессий напрямую.""" engine = create_async_engine( settings.database.url, echo=False, pool_pre_ping=True, ) return async_sessionmaker( engine, class_=AsyncSession, expire_on_commit=False, autoflush=False, ) # ============================================================================= # Данные для заполнения # ============================================================================= USERS_DATA = [ { "email": "admin@testgen.local", "password": "Admin123!", "full_name": "Администратор Системы", "role": UserRole.ADMIN, "department": "ИТ-отдел", }, { "email": "methodologist@testgen.local", "password": "Method123!", "full_name": "Елена Методистова", "role": UserRole.METHODOLOGIST, "department": "Учебно-методическое управление", }, { "email": "ivanov@testgen.local", "password": "Teacher123!", "full_name": "Иван Иванов", "role": UserRole.TEACHER, "department": "Кафедра физики", }, { "email": "petrova@testgen.local", "password": "Teacher123!", "full_name": "Мария Петрова", "role": UserRole.TEACHER, "department": "Кафедра биологии", }, { "email": "sidorov@testgen.local", "password": "Teacher123!", "full_name": "Алексей Сидоров", "role": UserRole.TEACHER, "department": "Кафедра математики", }, { "email": "assistant@testgen.local", "password": "Assist123!", "full_name": "Ольга Ассистентова", "role": UserRole.ASSISTANT, "department": "Кафедра физики", }, ] COURSES_DATA = [ { "code": "PHYS301", "name": "Квантовая механика", "description": ( "Курс квантовой механики для студентов 3 курса физического факультета. " "Охватывает основные принципы квантовой теории, волновую механику, " "операторный формализм и простые квантовые системы." ), "faculty": "Физический факультет", "department": "Кафедра теоретической физики", "semester": 5, "status": CourseStatus.ACTIVE, "passport": { "audience_description": ( "Студенты третьего курса физического факультета. " "Имеют хорошую математическую подготовку (линейная алгебра, " "дифференциальные уравнения, теория вероятностей). " "Знакомы с классической механикой и электродинамикой. " "Склонны к формальному математическому подходу, " "иногда упуская физический смысл явлений." ), "course_goals": ( "По завершении курса студент должен уметь:\n" "1) формулировать основные постулаты квантовой механики;\n" "2) решать уравнение Шрёдингера для простых потенциалов;\n" "3) интерпретировать волновую функцию и её физический смысл;\n" "4) применять операторный формализм для расчёта наблюдаемых величин;\n" "5) объяснять квантовую природу туннельного эффекта." ), "restrictions": ( "Избегать использования терминов без предварительного определения. " "Не углубляться в квантовую теорию поля и релятивистскую квантовую механику. " "Объём каждого вопроса — не более 200 слов. " "Не использовать задачи, требующие численного моделирования." ), "style_requirements": ( "Академический стиль с математической строгостью. " "Примеры — из реальных физических экспериментов " "(двухщелевой опыт, эффект Штерна-Герлаха, квантовая телепортация). " "Использовать обозначения Дирака для состояний |ψ⟩." ), }, "bloom_levels": [ BloomLevel.KNOWLEDGE, BloomLevel.COMPREHENSION, BloomLevel.APPLICATION, BloomLevel.ANALYSIS, ], "topics": [ { "name": "Волновая функция и её интерпретация", "description": "Понятие волновой функции, статистическая интерпретация Борна, нормировка", "key_concepts": ["волновая функция", "плотность вероятности", "нормировка", "принцип суперпозиции"], "subtopics": [ "Статистическая интерпретация Борна", "Нормировка волновой функции", "Принцип суперпозиции состояний", "Операторы физических величин", ], }, { "name": "Уравнение Шрёдингера", "description": "Временное и стационарное уравнение Шрёдингера, простые потенциалы", "key_concepts": ["уравнение Шрёдингера", "стационарные состояния", "потенциальная яма", "туннельный эффект"], "subtopics": [ "Временное уравнение Шрёдингера", "Стационарное уравнение Шрёдингера", "Частица в бесконечно глубокой яме", "Гармонический осциллятор", "Туннельный эффект", ], }, { "name": "Операторный формализм", "description": "Операторы, коммутаторы, собственные значения и собственные состояния", "key_concepts": ["оператор", "коммутатор", "собственное значение", "принцип неопределённости"], "subtopics": [ "Эрмитовы операторы", "Коммутационные соотношения", "Принцип неопределённости Гейзенберга", ], }, ], }, { "code": "BIO201", "name": "Молекулярная биология", "description": ( "Курс молекулярной биологии для студентов 2 курса биологического факультета. " "Изучение структуры и функций биомолекул, механизмов репликации, " "транскрипции и трансляции." ), "faculty": "Биологический факультет", "department": "Кафедра молекулярной биологии", "semester": 4, "status": CourseStatus.ACTIVE, "passport": { "audience_description": ( "Студенты второго курса биологического факультета. " "Знакомы с общей биологией, органической химией и основами биохимии. " "Склонны к запоминанию фактов без глубокого понимания молекулярных механизмов. " "Имеют базовые навыки работы с микроскопом и лабораторным оборудованием." ), "course_goals": ( "По завершении курса студент должен:\n" "1) понимать структуру и функции ДНК, РНК и белков;\n" "2) знать механизмы репликации, транскрипции и трансляции;\n" "3) разбираться в принципах регуляции экспрессии генов;\n" "4) уметь объяснять молекулярные основы наследственности." ), "restrictions": ( "Не использовать сложные биохимические формулы без пояснений. " "Избегать узкоспециальных терминов без предварительного определения. " "Не углубляться в медицинские аспекты и клинические случаи." ), "style_requirements": ( "Научный стиль с элементами научно-популярного. " "Примеры — из медицины, биотехнологий и генной инженерии. " "Использовать общепринятые аббревиатуры (ДНК, РНК, АТФ, ПЦР)." ), }, "bloom_levels": [ BloomLevel.KNOWLEDGE, BloomLevel.COMPREHENSION, BloomLevel.APPLICATION, ], "topics": [ { "name": "Структура и функции нуклеиновых кислот", "description": "ДНК, РНК, их структура и роль в клетке", "key_concepts": ["ДНК", "РНК", "нуклеотид", "двойная спираль", "комплементарность"], "subtopics": [ "Строение ДНК: первичная и вторичная структура", "Типы РНК: мРНК, тРНК, рРНК", "Принцип комплементарности", ], }, { "name": "Репликация ДНК", "description": "Процесс удвоения молекулы ДНК, ферменты репликации", "key_concepts": ["репликация", "ДНК-полимераза", "хеликаза", "фрагменты Оказаки"], "subtopics": [ "Ферменты репликации", "Стадии репликации", "Ошибки репликации и репарация", ], }, { "name": "Транскрипция и трансляция", "description": "Синтез РНК на матрице ДНК и синтез белка", "key_concepts": ["транскрипция", "трансляция", "рибосома", "генетический код"], "subtopics": [ "Транскрипция: инициация, элонгация, терминация", "Процессинг РНК", "Трансляция: рибосомы и тРНК", ], }, ], }, { "code": "MATH201", "name": "Линейная алгебра", "description": ( "Базовый курс линейной алгебры для студентов технических специальностей. " "Векторные пространства, матрицы, линейные преобразования." ), "faculty": "Математический факультет", "department": "Кафедра высшей математики", "semester": 3, "status": CourseStatus.ACTIVE, "passport": { "audience_description": ( "Студенты второго курса технических специальностей. " "Владеют базовой математикой, знакомы с аналитической геометрией. " "Склонны к алгоритмическому мышлению, испытывают трудности с абстрактными концепциями." ), "course_goals": ( "Освоение методов линейной алгебры, необходимых для решения " "инженерных и физических задач." ), "restrictions": ( "Использовать стандартные математические обозначения. " "Избегать излишней абстракции без практических примеров." ), "style_requirements": ( "Математически строгий, но с примерами из физики и инженерии. " "Использовать матричную запись." ), }, "bloom_levels": [ BloomLevel.KNOWLEDGE, BloomLevel.COMPREHENSION, BloomLevel.APPLICATION, BloomLevel.ANALYSIS, ], "topics": [ { "name": "Матрицы и определители", "description": "Операции с матрицами, определители, обратная матрица", "key_concepts": ["матрица", "определитель", "обратная матрица", "ранг"], "subtopics": [ "Операции над матрицами", "Определитель матрицы", "Обратная матрица", ], }, { "name": "Системы линейных уравнений", "description": "Методы решения СЛАУ", "key_concepts": ["СЛАУ", "метод Гаусса", "метод Крамера", "фундаментальная система решений"], "subtopics": [ "Метод Гаусса", "Правило Крамера", "Однородные и неоднородные системы", ], }, ], }, ] PROMPTS_DATA = [ { "name": "question_generation_v1", "prompt_type": PipelineStep.QUESTIONS, "description": "Базовая генерация тестовых вопросов с одним правильным ответом", "content": """Ты — опытный преподаватель высшего учебного заведения. Твоя задача — создавать качественные тестовые вопросы закрытого типа. КОНТЕКСТ КУРСА: Аудитория: {{audience}} Цели курса: {{course_goals}} Ограничения: {{restrictions}} Стиль: {{style}} ТЕМА: {{topic_name}} {{topic_description}} ПОДТЕМА: {{subtopic_name}} {{subtopic_description}} КЛЮЧЕВЫЕ ПОНЯТИЯ: {{key_concepts}} УРОВЕНЬ ТАКСОНОМИИ БЛУМА: {{bloom_level}} ДОСТУПНЫЕ УРОВНИ: {{bloom_levels}} Создай {{count}} тестовых вопросов закрытого типа с одним правильным ответом. Каждый вопрос должен соответствовать указанному уровню таксономии Блума. Ответ должен быть строго в формате JSON с массивом questions.""", "variables": { "audience": "Описание аудитории студентов", "course_goals": "Цели курса", "restrictions": "Ограничения по содержанию", "style": "Требования к стилю", "topic_name": "Название темы", "topic_description": "Описание темы", "subtopic_name": "Название подтемы", "subtopic_description": "Описание подтемы", "key_concepts": "Ключевые понятия (через запятую)", "bloom_level": "Целевой уровень таксономии Блума", "bloom_levels": "Доступные уровни таксономии", "count": "Количество вопросов", }, "response_format": { "type": "object", "properties": { "questions": { "type": "array", "items": { "type": "object", "properties": { "question_text": {"type": "string"}, "correct_answer": {"type": "string"}, "bloom_level": {"type": "string"}, }, "required": ["question_text", "correct_answer"], }, }, }, "required": ["questions"], }, }, { "name": "misconception_generation_v1", "prompt_type": PipelineStep.MISCONCEPTIONS, "description": "Генерация типичных учебных заблуждений студентов", "content": """Ты — опытный преподаватель, который знает типичные ошибки студентов. КОНТЕКСТ: Аудитория: {{audience}} Тема: {{topic_name}} Подтема: {{subtopic_name}} {{subtopic_description}} Перечисли {{count}} типичных заблуждений (ошибочных представлений) студентов при изучении темы "{{subtopic_name}}". Для каждого заблуждения укажи: 1. Формулировку ошибочного представления 2. Причину его возникновения (почему студенты так думают) Ответ должен быть в формате JSON с массивом misconceptions.""", "variables": { "audience": "Описание аудитории", "topic_name": "Название темы", "subtopic_name": "Название подтемы", "subtopic_description": "Описание подтемы", "count": "Количество заблуждений", }, "response_format": { "type": "object", "properties": { "misconceptions": { "type": "array", "items": { "type": "object", "properties": { "misconception_text": {"type": "string"}, "reason_explanation": {"type": "string"}, }, "required": ["misconception_text", "reason_explanation"], }, }, }, "required": ["misconceptions"], }, }, { "name": "distractor_generation_v1", "prompt_type": PipelineStep.DISTRACTORS, "description": "Генерация дистракторов на основе учебных заблуждений", "content": """Ты — эксперт по созданию тестовых заданий. ВОПРОС: {{question_text}} ПРАВИЛЬНЫЙ ОТВЕТ: {{correct_answer}} ЗАБЛУЖДЕНИЯ СТУДЕНТОВ: {{misconceptions}} Создай {{strong_count}} "хороших" дистракторов (основанных на заблуждениях) и {{weak_count}} "слабых" дистракторов (заведомо неверных). Ответ в формате JSON.""", "variables": { "question_text": "Текст вопроса", "correct_answer": "Правильный ответ", "misconceptions": "Список заблуждений с пояснениями", "strong_count": "Количество хороших дистракторов", "weak_count": "Количество слабых дистракторов", }, "response_format": {}, }, ] DEMO_QUESTIONS = [ { "question_text": "Какая интерпретация волновой функции была предложена Максом Борном?", "correct_answer": "Квадрат модуля волновой функции пропорционален плотности вероятности обнаружения частицы", "bloom_level": BloomLevel.KNOWLEDGE, "distractors": [ { "distractor_text": "Волновая функция описывает траекторию движения частицы в пространстве", "distractor_type": DistractorType.STRONG, "explanation": "Студенты часто путают квантовое описание с классическим, представляя частицу как точку на траектории.", }, { "distractor_text": "Волновая функция определяет точное положение частицы в данный момент времени", "distractor_type": DistractorType.STRONG, "explanation": "Заблуждение связано с непониманием вероятностной природы квантовой механики.", }, { "distractor_text": "Волновая функция описывает форму электронного облака вокруг ядра", "distractor_type": DistractorType.WEAK, "explanation": "Это слишком упрощённое и неточное представление.", }, { "distractor_text": "Волновая функция равна нулю в точках, где частица никогда не может находиться", "distractor_type": DistractorType.STRONG, "explanation": "Студенты путают волновую функцию с распределением вероятности.", }, ], }, { "question_text": "Какой фермент отвечает за расплетение двойной спирали ДНК при репликации?", "correct_answer": "Хеликаза (ДНК-хеликаза)", "bloom_level": BloomLevel.KNOWLEDGE, "distractors": [ { "distractor_text": "ДНК-полимераза", "distractor_type": DistractorType.STRONG, "explanation": "Студенты путают ферменты репликации: полимераза синтезирует новую цепь, а не расплетает.", }, { "distractor_text": "ДНК-лигаза", "distractor_type": DistractorType.STRONG, "explanation": "Лигаза сшивает фрагменты ДНК, но не расплетает спираль.", }, { "distractor_text": "РНК-полимераза", "distractor_type": DistractorType.STRONG, "explanation": "РНК-полимераза участвует в транскрипции, а не в репликации.", }, { "distractor_text": "Рибосома", "distractor_type": DistractorType.WEAK, "explanation": "Рибосома вообще не участвует в репликации ДНК.", }, ], }, ] DEMO_MISCONCEPTIONS = [ { "misconception_text": "Волновая функция описывает реальную физическую волну, подобную звуковой или водяной", "reason_explanation": "Студенты переносят интуитивное понимание волн из классической физики на квантовые объекты.", }, { "misconception_text": "Принцип неопределённости Гейзенберга связан с несовершенством измерительных приборов", "reason_explanation": "Студенты не понимают, что неопределённость — фундаментальное свойство природы, а не техническое ограничение.", }, { "misconception_text": "В квантовой механике частица всегда находится в определённом месте, просто мы не можем его точно измерить", "reason_explanation": "Студенты придерживаются скрытых параметров, отвергая вероятностную природу квантовой механики.", }, { "misconception_text": "Туннельный эффект означает, что частица «пробивает» потенциальный барьер, затрачивая энергию", "reason_explanation": "Студенты пытаются объяснить квантовое явление через классические аналогии с затратами энергии.", }, { "misconception_text": "ДНК-полимераза может начинать синтез новой цепи ДНК самостоятельно, без затравки", "reason_explanation": "Студенты не учитывают, что ДНК-полимераза требует 3'-OH группу для присоединения нуклеотидов.", }, { "misconception_text": "Транскрипция и трансляция происходят одновременно во всех организмах", "reason_explanation": "Студенты переносят механизм прокариот на эукариот, не учитывая наличие ядерной мембраны.", }, { "misconception_text": "Мутации всегда приводят к негативным последствиям для организма", "reason_explanation": "Студенты имеют упрощённое представление о мутациях как о всегда вредных изменениях.", }, { "misconception_text": "Генетический код одинаков для абсолютно всех живых организмов", "reason_explanation": "Студенты не знают об исключениях (митохондриальный код, код некоторых простейших).", }, ] def create_session_factory(): """Создаёт фабрику сессий напрямую.""" engine = create_async_engine( settings.database.url, echo=False, pool_pre_ping=True, ) return async_sessionmaker( engine, class_=AsyncSession, expire_on_commit=False, autoflush=False, ) async def seed_users(session) -> dict: """Создаёт пользователей. Возвращает словарь email -> user.""" user_repo = UserRepository(session) users = {} for data in USERS_DATA: existing = await user_repo.get_by_email(data["email"]) if existing: print(f" ↳ Пользователь уже существует: {data['email']}") users[data["email"]] = existing continue user = await user_repo.create( email=data["email"], password_hash=hash_password(data["password"]), full_name=data["full_name"], role=data["role"], department=data.get("department"), is_active=True, ) users[data["email"]] = user print(f" ✓ Создан пользователь: {data['full_name']} ({data['role']})") return users async def seed_prompts(session, admin_id) -> dict: """Создаёт предустановленные промты. Возвращает словарь name -> prompt.""" prompt_repo = PromptRepository(session) prompts = {} for data in PROMPTS_DATA: existing = await prompt_repo.list(filters={"name": data["name"]}) if existing: print(f" ↳ Промт уже существует: {data['name']}") prompts[data["name"]] = existing[0] continue prompt = await prompt_repo.create( name=data["name"], prompt_type=data["prompt_type"], description=data["description"], is_system=True, is_active=True, ) # Создание первой версии промта from testgen.domain.models.prompt import PromptVersion version = PromptVersion( prompt_id=prompt.id, version_number=1, content=data["content"], variables=data["variables"], response_format=data["response_format"], change_description="Начальная версия", is_current=True, created_by=admin_id, ) session.add(version) prompts[data["name"]] = prompt print(f" ✓ Создан промт: {data['name']} (v1)") return prompts async def seed_courses(session, teacher_id) -> list: """Создаёт курсы, паспорта, темы и подтемы.""" course_repo = CourseRepository(session) passport_repo = PassportRepository(session) topic_repo = TopicRepository(session) subtopic_repo = SubtopicRepository(session) courses = [] for data in COURSES_DATA: existing = await course_repo.get_by_code(data["code"]) if existing: print(f" ↳ Курс уже существует: {data['name']} ({data['code']})") courses.append(existing) continue # Создание курса course = await course_repo.create( code=data["code"], name=data["name"], description=data.get("description"), faculty=data.get("faculty"), department=data.get("department"), semester=data.get("semester"), status=data.get("status", CourseStatus.ACTIVE), created_by=teacher_id, ) print(f" ✓ Создан курс: {course.name} ({course.code})") # Создание паспорта passport_data = data["passport"] passport = await passport_repo.create( course_id=course.id, version_number=1, audience_description=passport_data["audience_description"], course_goals=passport_data["course_goals"], restrictions=passport_data["restrictions"], style_requirements=passport_data["style_requirements"], is_active=True, created_by=teacher_id, ) print(f" ✓ Паспорт курса (v1)") # Создание уровней таксономии Блума from testgen.domain.models.passport import BloomTaxonomyLevel for level in data.get("bloom_levels", []): bloom = BloomTaxonomyLevel( course_id=course.id, level=level, is_enabled=True, ) session.add(bloom) print(f" ✓ Уровни таксономии: {len(data.get('bloom_levels', []))}") # Создание тем и подтем for i, topic_data in enumerate(data.get("topics", [])): topic = await topic_repo.create( course_id=course.id, name=topic_data["name"], description=topic_data.get("description"), key_concepts=topic_data.get("key_concepts", []), order_number=i + 1, ) print(f" ✓ Тема {i+1}: {topic.name}") for j, subtopic_name in enumerate(topic_data.get("subtopics", [])): subtopic = await subtopic_repo.create( topic_id=topic.id, name=subtopic_name, order_number=j + 1, ) print(f" ✓ Подтема {j+1}: {subtopic.name}") courses.append(course) return courses async def seed_questions_and_misconceptions(session, courses, teacher_id) -> None: """Создаёт демо-вопросы и учебные заблуждения.""" question_repo = QuestionRepository(session) distractor_repo = DistractorRepository(session) misconception_repo = MisconceptionRepository(session) subtopic_repo = SubtopicRepository(session) for course in courses: # Получаем первую тему и подтему from testgen.domain.models.topic import Topic, Subtopic from sqlalchemy import select stmt = ( select(Subtopic) .join(Topic) .where(Topic.course_id == course.id) .limit(1) ) result = await session.execute(stmt) first_subtopic = result.scalar_one_or_none() if not first_subtopic: continue # Создание демо-вопросов for q_data in DEMO_QUESTIONS: question = await question_repo.create( subtopic_id=first_subtopic.id, question_text=q_data["question_text"], correct_answer=q_data["correct_answer"], bloom_level=q_data["bloom_level"], status=QuestionStatus.APPROVED, created_by=teacher_id, ) print(f" ✓ Вопрос: {q_data['question_text'][:60]}...") # Создание дистракторов for d_data in q_data.get("distractors", []): await distractor_repo.create( question_id=question.id, distractor_text=d_data["distractor_text"], distractor_type=d_data["distractor_type"], explanation=d_data.get("explanation"), source="manual", ) # Создание учебных заблуждений for m_data in DEMO_MISCONCEPTIONS: await misconception_repo.create( subtopic_id=first_subtopic.id, misconception_text=m_data["misconception_text"], reason_explanation=m_data["reason_explanation"], source="manual", status="accepted", ) print(f" ✓ Создано {len(DEMO_MISCONCEPTIONS)} учебных заблуждений") async def main() -> None: """Точка входа.""" parser = argparse.ArgumentParser(description="Наполнение БД тестовыми данными") parser.add_argument( "--env", choices=["development", "production"], default="development", help="Окружение", ) args = parser.parse_args() print() print("=" * 60) print(" Наполнение базы данных TestGen тестовыми данными") print(f" Окружение: {args.env}") print("=" * 60) print() session_factory = create_session_factory() async with session_factory() as session: try: # 1. Пользователи print("📝 Создание пользователей...") users = await seed_users(session) await session.flush() print(f" Всего пользователей: {len(users)}") print() # 2. Промты print("📋 Создание предустановленных промтов...") admin = users.get("admin@testgen.local") if admin: prompts = await seed_prompts(session, admin.id) await session.flush() print(f" Всего промтов: {len(prompts)}") print() # 3. Курсы print("📚 Создание курсов, тем и подтем...") teacher = users.get("ivanov@testgen.local") if teacher: courses = await seed_courses(session, teacher.id) await session.flush() print(f" Всего курсов: {len(courses)}") print() # 4. Вопросы и заблуждения print("❓ Создание демо-вопросов и заблуждений...") if teacher: await seed_questions_and_misconceptions(session, courses, teacher.id) await session.flush() print() await session.commit() print("=" * 60) print(" ✅ Наполнение базы данных завершено успешно!") print("=" * 60) print() print("Данные для входа:") print(" Администратор: admin@testgen.local / Admin123!") print(" Преподаватель: ivanov@testgen.local / Teacher123!") print(" Методист: methodologist@testgen.local / Method123!") print() except Exception as e: await session.rollback() print() print(f"❌ Ошибка при наполнении БД: {e}") print() print("Проверьте:") print(" 1. База данных PostgreSQL запущена") print(" 2. Миграции применены (uv run alembic upgrade head)") print(" 3. Параметры в .env верны") import traceback traceback.print_exc() sys.exit(1) if __name__ == "__main__": asyncio.run(main())