371 lines
14 KiB
Python
371 lines
14 KiB
Python
from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile
|
||
from pydantic import BaseModel
|
||
|
||
from app.models.vacancy import VacancyCreate, VacancyRead, VacancyUpdate
|
||
from app.services.vacancy_service import VacancyService
|
||
from app.services.vacancy_parser_service import vacancy_parser_service
|
||
|
||
router = APIRouter(prefix="/vacancies", tags=["vacancies"])
|
||
|
||
|
||
class VacancyParseResponse(BaseModel):
|
||
"""Ответ на запрос парсинга вакансии"""
|
||
message: str
|
||
parsed_data: dict | None = None
|
||
task_id: str | None = None
|
||
|
||
|
||
@router.post("/", response_model=VacancyRead)
|
||
async def create_vacancy(
|
||
vacancy: VacancyCreate, vacancy_service: VacancyService = Depends(VacancyService)
|
||
):
|
||
return await vacancy_service.create_vacancy(vacancy)
|
||
|
||
|
||
@router.get("/", response_model=list[VacancyRead])
|
||
async def get_vacancies(
|
||
skip: int = Query(0, ge=0),
|
||
limit: int = Query(100, ge=1, le=1000),
|
||
active_only: bool = Query(False),
|
||
title: str | None = Query(None),
|
||
company_name: str | None = Query(None),
|
||
area_name: str | None = Query(None),
|
||
vacancy_service: VacancyService = Depends(VacancyService),
|
||
):
|
||
if any([title, company_name, area_name]):
|
||
return await vacancy_service.search_vacancies(
|
||
title=title,
|
||
company_name=company_name,
|
||
area_name=area_name,
|
||
skip=skip,
|
||
limit=limit,
|
||
)
|
||
|
||
if active_only:
|
||
return await vacancy_service.get_active_vacancies(skip=skip, limit=limit)
|
||
|
||
return await vacancy_service.get_all_vacancies(skip=skip, limit=limit)
|
||
|
||
|
||
@router.get("/{vacancy_id}", response_model=VacancyRead)
|
||
async def get_vacancy(
|
||
vacancy_id: int, vacancy_service: VacancyService = Depends(VacancyService)
|
||
):
|
||
vacancy = await vacancy_service.get_vacancy(vacancy_id)
|
||
if not vacancy:
|
||
raise HTTPException(status_code=404, detail="Vacancy not found")
|
||
return vacancy
|
||
|
||
|
||
@router.put("/{vacancy_id}", response_model=VacancyRead)
|
||
async def update_vacancy(
|
||
vacancy_id: int,
|
||
vacancy: VacancyUpdate,
|
||
vacancy_service: VacancyService = Depends(VacancyService),
|
||
):
|
||
updated_vacancy = await vacancy_service.update_vacancy(vacancy_id, vacancy)
|
||
if not updated_vacancy:
|
||
raise HTTPException(status_code=404, detail="Vacancy not found")
|
||
return updated_vacancy
|
||
|
||
|
||
@router.delete("/{vacancy_id}")
|
||
async def delete_vacancy(
|
||
vacancy_id: int, vacancy_service: VacancyService = Depends(VacancyService)
|
||
):
|
||
success = await vacancy_service.delete_vacancy(vacancy_id)
|
||
if not success:
|
||
raise HTTPException(status_code=404, detail="Vacancy not found")
|
||
return {"message": "Vacancy deleted successfully"}
|
||
|
||
|
||
@router.patch("/{vacancy_id}/archive", response_model=VacancyRead)
|
||
async def archive_vacancy(
|
||
vacancy_id: int, vacancy_service: VacancyService = Depends(VacancyService)
|
||
):
|
||
archived_vacancy = await vacancy_service.archive_vacancy(vacancy_id)
|
||
if not archived_vacancy:
|
||
raise HTTPException(status_code=404, detail="Vacancy not found")
|
||
return archived_vacancy
|
||
|
||
|
||
@router.post("/parse-file", response_model=VacancyParseResponse)
|
||
async def parse_vacancy_from_file(
|
||
file: UploadFile = File(...),
|
||
create_vacancy: bool = Query(False, description="Создать вакансию после парсинга"),
|
||
vacancy_service: VacancyService = Depends(VacancyService),
|
||
):
|
||
"""
|
||
Парсинг вакансии из загруженного файла (PDF, DOCX, RTF, TXT)
|
||
|
||
Args:
|
||
file: Файл вакансии
|
||
create_vacancy: Создать вакансию в БД после парсинга
|
||
|
||
Returns:
|
||
VacancyParseResponse: Результат парсинга
|
||
"""
|
||
|
||
# Проверяем формат файла
|
||
if not file.filename:
|
||
raise HTTPException(status_code=400, detail="Имя файла не указано")
|
||
|
||
file_extension = file.filename.lower().split('.')[-1]
|
||
supported_formats = ['pdf', 'docx', 'rtf', 'txt']
|
||
|
||
if file_extension not in supported_formats:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail=f"Неподдерживаемый формат файла. Поддерживаются: {', '.join(supported_formats)}"
|
||
)
|
||
|
||
# Проверяем размер файла (максимум 10MB)
|
||
file_content = await file.read()
|
||
if len(file_content) > 10 * 1024 * 1024:
|
||
raise HTTPException(status_code=400, detail="Файл слишком большой (максимум 10MB)")
|
||
|
||
try:
|
||
# Извлекаем текст из файла
|
||
raw_text = vacancy_parser_service.extract_text_from_file(file_content, file.filename)
|
||
|
||
if not raw_text.strip():
|
||
raise HTTPException(status_code=400, detail="Не удалось извлечь текст из файла")
|
||
|
||
# Парсим с помощью AI
|
||
parsed_data = await vacancy_parser_service.parse_vacancy_with_ai(raw_text)
|
||
|
||
# Если нужно создать вакансию, создаем её
|
||
created_vacancy = None
|
||
if create_vacancy:
|
||
try:
|
||
vacancy_create = VacancyCreate(**parsed_data)
|
||
created_vacancy = await vacancy_service.create_vacancy(vacancy_create)
|
||
except Exception as e:
|
||
# Возвращаем парсинг, но предупреждаем об ошибке создания
|
||
return VacancyParseResponse(
|
||
message=f"Парсинг выполнен, но ошибка при создании вакансии: {str(e)}",
|
||
parsed_data=parsed_data
|
||
)
|
||
|
||
response_message = "Парсинг выполнен успешно"
|
||
if created_vacancy:
|
||
response_message += f". Вакансия создана с ID: {created_vacancy.id}"
|
||
|
||
return VacancyParseResponse(
|
||
message=response_message,
|
||
parsed_data=parsed_data
|
||
)
|
||
|
||
except ValueError as e:
|
||
raise HTTPException(status_code=400, detail=str(e))
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=f"Ошибка при парсинге вакансии: {str(e)}")
|
||
|
||
|
||
@router.post("/parse-text", response_model=VacancyParseResponse)
|
||
async def parse_vacancy_from_text(
|
||
text: str = Query(..., description="Текст вакансии для парсинга"),
|
||
create_vacancy: bool = Query(False, description="Создать вакансию после парсинга"),
|
||
vacancy_service: VacancyService = Depends(VacancyService),
|
||
):
|
||
"""
|
||
Парсинг вакансии из текста
|
||
|
||
Args:
|
||
text: Текст вакансии
|
||
create_vacancy: Создать вакансию в БД после парсинга
|
||
|
||
Returns:
|
||
VacancyParseResponse: Результат парсинга
|
||
"""
|
||
|
||
if not text.strip():
|
||
raise HTTPException(status_code=400, detail="Текст вакансии не может быть пустым")
|
||
|
||
if len(text) > 50000: # Ограничение на длину текста
|
||
raise HTTPException(status_code=400, detail="Текст слишком длинный (максимум 50000 символов)")
|
||
|
||
try:
|
||
# Парсим с помощью AI
|
||
parsed_data = await vacancy_parser_service.parse_vacancy_with_ai(text)
|
||
|
||
# Если нужно создать вакансию, создаем её
|
||
created_vacancy = None
|
||
if create_vacancy:
|
||
try:
|
||
vacancy_create = VacancyCreate(**parsed_data)
|
||
created_vacancy = await vacancy_service.create_vacancy(vacancy_create)
|
||
except Exception as e:
|
||
return VacancyParseResponse(
|
||
message=f"Парсинг выполнен, но ошибка при создании вакансии: {str(e)}",
|
||
parsed_data=parsed_data
|
||
)
|
||
|
||
response_message = "Парсинг выполнен успешно"
|
||
if created_vacancy:
|
||
response_message += f". Вакансия создана с ID: {created_vacancy.id}"
|
||
|
||
return VacancyParseResponse(
|
||
message=response_message,
|
||
parsed_data=parsed_data
|
||
)
|
||
|
||
except ValueError as e:
|
||
raise HTTPException(status_code=400, detail=str(e))
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=f"Ошибка при парсинге вакансии: {str(e)}")
|
||
|
||
|
||
@router.get("/parse-formats")
|
||
async def get_supported_formats():
|
||
"""
|
||
Получить список поддерживаемых форматов файлов для парсинга вакансий
|
||
|
||
Returns:
|
||
dict: Информация о поддерживаемых форматах
|
||
"""
|
||
return {
|
||
"supported_formats": [
|
||
{
|
||
"extension": "pdf",
|
||
"description": "PDF документы",
|
||
"max_size_mb": 10
|
||
},
|
||
{
|
||
"extension": "docx",
|
||
"description": "Microsoft Word документы",
|
||
"max_size_mb": 10
|
||
},
|
||
{
|
||
"extension": "rtf",
|
||
"description": "Rich Text Format",
|
||
"max_size_mb": 10
|
||
},
|
||
{
|
||
"extension": "txt",
|
||
"description": "Текстовые файлы",
|
||
"max_size_mb": 10
|
||
}
|
||
],
|
||
"features": [
|
||
"Автоматическое извлечение текста из файлов",
|
||
"AI-парсинг структурированной информации",
|
||
"Создание вакансии в базе данных",
|
||
"Валидация данных"
|
||
]
|
||
}
|
||
|
||
|
||
@router.post("/parse-file-async", response_model=dict)
|
||
async def parse_vacancy_from_file_async(
|
||
file: UploadFile = File(...),
|
||
create_vacancy: str = Form("false", description="Создать вакансию после парсинга"),
|
||
):
|
||
"""
|
||
Асинхронный парсинг вакансии из загруженного файла (PDF, DOCX, RTF, TXT)
|
||
|
||
Args:
|
||
file: Файл вакансии
|
||
create_vacancy: Создать вакансию в БД после парсинга
|
||
|
||
Returns:
|
||
dict: ID задачи для отслеживания статуса
|
||
"""
|
||
import base64
|
||
from celery_worker.tasks import parse_vacancy_task
|
||
|
||
# Проверяем формат файла
|
||
if not file.filename:
|
||
raise HTTPException(status_code=400, detail="Имя файла не указано")
|
||
|
||
file_extension = file.filename.lower().split('.')[-1]
|
||
supported_formats = ['pdf', 'docx', 'rtf', 'txt']
|
||
|
||
if file_extension not in supported_formats:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail=f"Неподдерживаемый формат файла. Поддерживаются: {', '.join(supported_formats)}"
|
||
)
|
||
|
||
# Проверяем размер файла (максимум 10MB)
|
||
file_content = await file.read()
|
||
if len(file_content) > 10 * 1024 * 1024:
|
||
raise HTTPException(status_code=400, detail="Файл слишком большой (максимум 10MB)")
|
||
|
||
try:
|
||
# Кодируем содержимое файла в base64 для передачи в Celery
|
||
file_content_base64 = base64.b64encode(file_content).decode('utf-8')
|
||
|
||
# Конвертируем строку в boolean
|
||
create_vacancy_bool = create_vacancy.lower() in ('true', '1', 'yes', 'on')
|
||
|
||
# Запускаем асинхронную задачу
|
||
task = parse_vacancy_task.delay(
|
||
file_content_base64=file_content_base64,
|
||
filename=file.filename,
|
||
create_vacancy=create_vacancy_bool
|
||
)
|
||
|
||
return {
|
||
"message": "Задача парсинга запущена",
|
||
"task_id": task.id,
|
||
"status": "pending",
|
||
"check_status_url": f"/api/v1/vacancies/parse-status/{task.id}"
|
||
}
|
||
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=f"Ошибка при запуске парсинга: {str(e)}")
|
||
|
||
|
||
@router.get("/parse-status/{task_id}")
|
||
async def get_parse_status(task_id: str):
|
||
"""
|
||
Получить статус асинхронной задачи парсинга вакансии
|
||
|
||
Args:
|
||
task_id: ID задачи
|
||
|
||
Returns:
|
||
dict: Статус задачи и результат (если завершена)
|
||
"""
|
||
from celery_worker.celery_app import celery_app
|
||
|
||
try:
|
||
task = celery_app.AsyncResult(task_id)
|
||
|
||
if task.state == 'PENDING':
|
||
response = {
|
||
'task_id': task_id,
|
||
'state': task.state,
|
||
'status': 'Задача ожидает выполнения...',
|
||
'progress': 0
|
||
}
|
||
elif task.state == 'PROGRESS':
|
||
response = {
|
||
'task_id': task_id,
|
||
'state': task.state,
|
||
'status': task.info.get('status', ''),
|
||
'progress': task.info.get('progress', 0)
|
||
}
|
||
elif task.state == 'SUCCESS':
|
||
response = {
|
||
'task_id': task_id,
|
||
'state': task.state,
|
||
'status': 'completed',
|
||
'progress': 100,
|
||
'result': task.result
|
||
}
|
||
else: # FAILURE
|
||
response = {
|
||
'task_id': task_id,
|
||
'state': task.state,
|
||
'status': 'failed',
|
||
'progress': 0,
|
||
'error': str(task.info)
|
||
}
|
||
|
||
return response
|
||
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=f"Ошибка при получении статуса задачи: {str(e)}")
|