initial commit
Build and Push Docker Images / build-and-push (push) Failing after 3m33s

This commit is contained in:
admin committed 2026-11-07 16:39:43 +03:00
commit 46af290db3
100 files changed
+16663

No files matched your search

+1
View File
@@ -0,0 +1 @@
+428
View File
@@ -0,0 +1,428 @@
from config.settings import settings
from fastapi import APIRouter, Depends, HTTPException
from models.ai import (
AutoUpdateBibleRequest,
AutoUpdateBibleResponse,
CleanupDuplicatesResponse,
ExtractedEntities,
ExtractEntitiesRequest,
ExtractEntitiesResponse,
GenerateImageRequest,
GenerateImageResponse,
GenerateRequest,
GenerateResponse,
PreviewContextRequest,
PreviewContextResponse,
)
from models.bible import Character, Event, Location, StoryBible
from models.user import User
from services.bible_service import bible_service
from services.context_builder import context_builder
from services.deduplication import deduplication_service
from services.entity_extractor import entity_extractor
from services.image_generator import image_generator
from services.llm_router import llm_router
from services.model_config import model_config
from routers.auth import get_current_user
router = APIRouter()
def _get_user_project(project_id: str, current_user: User) -> None:
"""Проверить что проект принадлежит пользователю"""
from services.project_service import project_service
project = project_service.get_project(project_id)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
if project.user_id != current_user.id:
raise HTTPException(status_code=403, detail="Access denied")
@router.get("/ai/providers/status")
async def get_providers_status():
"""Проверить, какие провайдеры настроены и готовы к использованию"""
return {
"providers": settings.validate(),
"openrouter_configured": bool(
settings.OPENROUTER_API_KEY
and settings.OPENROUTER_API_KEY != "your_api_key_here"
),
"openrouter_url": settings.OPENROUTER_BASE_URL,
"ollama_url": settings.OLLAMA_BASE_URL,
}
@router.get("/ai/models")
async def get_available_models(provider: str = "ollama"):
"""Получить список доступных моделей для указанного провайдера"""
try:
models = model_config.get_available_models(provider)
default_model = model_config.get_default_model(provider)
return {
"provider": provider,
"default_model": default_model,
"models": models,
}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/ai/generate", response_model=GenerateResponse)
async def generate_text(request: GenerateRequest):
try:
# Обогащаем промпт контекстом из Story Bible
enriched_prompt = context_builder.enrich_prompt(
original_prompt=request.prompt,
selected_text=request.selected_text or "",
bible=request.story_bible,
)
# Генерируем текст с обогащенным промптом
generated_text = await llm_router.generate(
prompt=enriched_prompt,
provider=request.provider,
model=request.model,
api_key=request.api_key,
preset=request.preset,
options=request.options,
)
return GenerateResponse(
generated_text=generated_text,
provider=request.provider,
model=request.model or "default",
preset=request.preset,
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/ai/preview-context", response_model=PreviewContextResponse)
async def preview_context(request: PreviewContextRequest):
"""Предпросмотр контекста, который будет добавлен к промпту"""
mentioned = context_builder.extract_mentioned_entities(
text=request.text,
bible=request.story_bible,
)
context_str = context_builder.build_context_string(mentioned)
return PreviewContextResponse(
context_string=context_str,
mentioned_characters=[c.name for c in mentioned["characters"]],
mentioned_locations=[l.name for l in mentioned["locations"]],
mentioned_events=[e.title for e in mentioned["events"]],
)
@router.post("/ai/extract-entities", response_model=ExtractEntitiesResponse)
async def extract_entities(request: ExtractEntitiesRequest):
"""Извлечение сущностей из текста с помощью AI"""
try:
# Используем OpenRouter с моделью, оптимальной для извлечения
provider = "openrouter"
model = "qwen/qwen-2.5-7b-instruct"
if (
not settings.OPENROUTER_API_KEY
or settings.OPENROUTER_API_KEY == "your_api_key_here"
):
raise ValueError(
"OpenRouter API key not configured. "
"Entity extraction requires a cloud model. "
"Please set OPENROUTER_API_KEY in backend/.env"
)
entities = await entity_extractor.extract_entities(
text=request.text,
existing_bible=request.existing_bible,
provider=provider,
model=model,
)
return ExtractEntitiesResponse(
entities=ExtractedEntities(**entities),
model_used=model,
provider=provider,
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/ai/auto-update-bible", response_model=AutoUpdateBibleResponse)
async def auto_update_bible(
request: AutoUpdateBibleRequest, current_user: User = Depends(get_current_user)
):
"""Автоматическое обновление Story Bible при сохранении сцены"""
if not request.auto_update_enabled:
return AutoUpdateBibleResponse(updated=False, message="Auto-update disabled")
_get_user_project(request.project_id, current_user)
try:
# Получаем текущую Story Bible проекта
current_bible = bible_service.get_bible(request.project_id, current_user.id)
# Извлекаем сущности из текста сцены
entities = await entity_extractor.extract_entities(
text=request.scene_text,
existing_bible=current_bible,
provider="openrouter",
model="qwen/qwen-2.5-7b-instruct",
)
# Добавляем новые сущности в Bible
new_chars_count = 0
new_locs_count = 0
new_events_count = 0
# Добавляем новых персонажей С ДЕДУПЛИКАЦИЕЙ
for char_data in entities.get("new_characters", []):
try:
char_name = char_data.get("name", "")
if not char_name:
continue
# Ищем существующего персонажа с похожим именем
existing_char = deduplication_service.find_existing_character(
char_name, current_bible
)
if existing_char:
# Объединяем данные с существующим персонажем
deduplication_service.merge_character_data(existing_char, char_data)
print(f"Merged character: {char_name} -> {existing_char.name}")
else:
# Создаем нового персонажа
character = Character(**char_data)
current_bible.characters.append(character)
new_chars_count += 1
print(f"Added new character: {char_name}")
except Exception as e:
print(f"Failed to process character: {e}")
# Добавляем новые локации С ДЕДУПЛИКАЦИЕЙ
for loc_data in entities.get("new_locations", []):
try:
loc_name = loc_data.get("name", "")
if not loc_name:
continue
existing_loc = deduplication_service.find_existing_location(
loc_name, current_bible
)
if existing_loc:
# Обновляем описание, если новое длиннее
new_desc = loc_data.get("description", "")
if new_desc and len(new_desc) > len(existing_loc.description):
existing_loc.description = new_desc
new_atm = loc_data.get("atmosphere", "")
if new_atm and not existing_loc.atmosphere:
existing_loc.atmosphere = new_atm
print(f"Merged location: {loc_name} -> {existing_loc.name}")
else:
location = Location(**loc_data)
current_bible.locations.append(location)
new_locs_count += 1
print(f"Added new location: {loc_name}")
except Exception as e:
print(f"Failed to process location: {e}")
# Добавляем новые события С ДЕДУПЛИКАЦИЕЙ
for event_data in entities.get("new_events", []):
try:
event_title = event_data.get("title", "")
if not event_title:
continue
existing_event = deduplication_service.find_existing_event(
event_title, current_bible
)
if existing_event:
# Обновляем описание, если новое длиннее
new_desc = event_data.get("description", "")
if new_desc and len(new_desc) > len(existing_event.description):
existing_event.description = new_desc
print(f"Merged event: {event_title} -> {existing_event.title}")
else:
event = Event(**event_data)
current_bible.events.append(event)
new_events_count += 1
print(f"Added new event: {event_title}")
except Exception as e:
print(f"Failed to process event: {e}")
# Обновляем черты существующих персонажей
for trait_update in entities.get("updated_traits", []):
char_name = trait_update.get("character_name")
new_traits = trait_update.get("new_traits", [])
for char in current_bible.characters:
if char.name.lower() == (char_name or "").lower():
# Добавляем новые черты, которых еще нет
for trait in new_traits:
if trait not in char.traits:
char.traits.append(trait)
# Сохраняем обновленную Bible
if new_chars_count > 0 or new_locs_count > 0 or new_events_count > 0:
bible_service.update_bible(
request.project_id, current_bible, current_user.id
)
message = (
f"Story Bible обновлена: +{new_chars_count} персонажей, "
f"+{new_locs_count} локаций, +{new_events_count} событий"
)
else:
message = "Новых сущностей не обнаружено"
return AutoUpdateBibleResponse(
updated=True,
new_characters=new_chars_count,
new_locations=new_locs_count,
new_events=new_events_count,
message=message,
)
except Exception as e:
print(f"Auto-update bible error: {e}")
return AutoUpdateBibleResponse(
updated=False,
message=f"Ошибка при обновлении: {str(e)}",
)
@router.post("/ai/extract-entities/test")
async def test_extraction():
"""Тестовый эндпоинт с примером текста"""
test_text = """
Алекс вошел в таверну "Серебряный дракон". Внутри пахло жареным мясом и элем.
За стойкой стоял хозяин — крепкий мужчина по имени Гаррет с рыжей бородой.
В углу сидела таинственная женщина в красном плаще — Алекс узнал Лиру.
"""
sample_bible = StoryBible(
characters=[
Character(name="Алекс", description="Главный герой", role="protagonist"),
Character(
name="Лира", description="Таинственная проводница", role="supporting"
),
]
)
try:
entities = await entity_extractor.extract_entities(
text=test_text,
existing_bible=sample_bible,
provider="openrouter",
model="qwen/qwen-2.5-7b-instruct",
)
return {
"test_text": test_text,
"extracted": entities,
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post(
"/projects/{project_id}/bible/cleanup-duplicates",
response_model=CleanupDuplicatesResponse,
)
async def cleanup_duplicates(
project_id: str, current_user: User = Depends(get_current_user)
):
"""Очистка дубликатов в Story Bible"""
_get_user_project(project_id, current_user)
try:
bible = bible_service.get_bible(project_id, current_user.id)
# Удаляем дубликаты персонажей
unique_chars = []
removed_chars = 0
for char in bible.characters:
is_duplicate = False
for existing in unique_chars:
if deduplication_service.is_similar_name(char.name, existing.name):
# Объединяем данные
deduplication_service.merge_character_data(
existing, char.model_dump()
)
is_duplicate = True
removed_chars += 1
break
if not is_duplicate:
unique_chars.append(char)
bible.characters = unique_chars
# Удаляем дубликаты локаций
unique_locs = []
removed_locs = 0
for loc in bible.locations:
is_duplicate = False
for existing in unique_locs:
if deduplication_service.is_similar_name(loc.name, existing.name):
is_duplicate = True
removed_locs += 1
break
if not is_duplicate:
unique_locs.append(loc)
bible.locations = unique_locs
# Удаляем дубликаты событий
unique_events = []
removed_events = 0
for event in bible.events:
is_duplicate = False
for existing in unique_events:
if deduplication_service.is_similar_name(event.title, existing.title):
is_duplicate = True
removed_events += 1
break
if not is_duplicate:
unique_events.append(event)
bible.events = unique_events
# Сохраняем очищенную Bible
bible_service.update_bible(project_id, bible, current_user.id)
return CleanupDuplicatesResponse(
removed_characters=removed_chars,
removed_locations=removed_locs,
removed_events=removed_events,
message=f"Удалено дубликатов: {removed_chars} персонажей, {removed_locs} локаций, {removed_events} событий",
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/ai/generate-image", response_model=GenerateImageResponse)
async def generate_image(
request: GenerateImageRequest, current_user: User = Depends(get_current_user)
):
"""Генерация изображения по промпту"""
try:
result = await image_generator.generate_image(
prompt=request.prompt,
project_id=request.project_id,
user_id=current_user.id,
document_id=request.document_id,
document_type=request.document_type,
model=request.model,
size=request.size,
)
return result
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
+50
View File
@@ -0,0 +1,50 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError, jwt
from models.user import Token, User, UserCreate, UserLogin
from services.auth_service import ALGORITHM, SECRET_KEY, auth_service
router = APIRouter()
security = HTTPBearer()
@router.post("/auth/register", response_model=Token)
async def register(user_create: UserCreate):
try:
user = auth_service.create_user(user_create)
return auth_service.get_token(user.username, user_create.password)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/auth/login", response_model=Token)
async def login(user_login: UserLogin):
try:
return auth_service.get_token(user_login.username, user_login.password)
except ValueError:
raise HTTPException(status_code=401, detail="Invalid username or password")
def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
) -> User:
try:
payload = jwt.decode(
credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM]
)
username: str = payload.get("sub")
user_id: str = payload.get("user_id")
if username is None or user_id is None:
raise HTTPException(status_code=401, detail="Invalid token")
user = auth_service.get_user_by_username(username)
if not user or user.id != user_id:
raise HTTPException(status_code=401, detail="User not found")
return user
except JWTError:
raise HTTPException(status_code=401, detail="Invalid token")
@router.get("/auth/me", response_model=User)
async def get_me(current_user: User = Depends(get_current_user)):
return current_user
+83
View File
@@ -0,0 +1,83 @@
from fastapi import APIRouter, Depends, HTTPException
from models.bible import Character, Event, Location, StoryBible, StoryThread
from models.user import User
from services.bible_service import bible_service
from routers.auth import get_current_user
router = APIRouter()
def _get_user_project(project_id: str, current_user: User) -> None:
"""Проверить что проект принадлежит пользователю"""
from services.project_service import project_service
project = project_service.get_project(project_id)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
if project.user_id != current_user.id:
raise HTTPException(status_code=403, detail="Access denied")
@router.get("/projects/{project_id}/bible", response_model=StoryBible)
async def get_story_bible(
project_id: str, current_user: User = Depends(get_current_user)
):
"""Получить Story Bible проекта"""
_get_user_project(project_id, current_user)
return bible_service.get_bible(project_id, current_user.id)
@router.put("/projects/{project_id}/bible", response_model=StoryBible)
async def update_story_bible(
project_id: str,
bible: StoryBible,
current_user: User = Depends(get_current_user),
):
"""Обновить всю Story Bible проекта"""
_get_user_project(project_id, current_user)
return bible_service.update_bible(project_id, bible, current_user.id)
@router.post("/projects/{project_id}/bible/characters", response_model=Character)
async def add_character(
project_id: str,
character: Character,
current_user: User = Depends(get_current_user),
):
"""Добавить нового персонажа в проект"""
_get_user_project(project_id, current_user)
return bible_service.add_character(project_id, character, current_user.id)
@router.post("/projects/{project_id}/bible/locations", response_model=Location)
async def add_location(
project_id: str,
location: Location,
current_user: User = Depends(get_current_user),
):
"""Добавить новую локацию в проект"""
_get_user_project(project_id, current_user)
return bible_service.add_location(project_id, location, current_user.id)
@router.post("/projects/{project_id}/bible/events", response_model=Event)
async def add_event(
project_id: str,
event: Event,
current_user: User = Depends(get_current_user),
):
"""Добавить новое событие в проект"""
_get_user_project(project_id, current_user)
return bible_service.add_event(project_id, event, current_user.id)
@router.post("/projects/{project_id}/bible/threads", response_model=StoryThread)
async def add_thread(
project_id: str,
thread: StoryThread,
current_user: User = Depends(get_current_user),
):
"""Добавить новую сюжетную нить в проект"""
_get_user_project(project_id, current_user)
return bible_service.add_thread(project_id, thread, current_user.id)
+151
View File
@@ -0,0 +1,151 @@
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, HTTPException
from models.document import DocumentNode
from models.user import User
from pydantic import BaseModel
from services.document_service import document_service
from routers.auth import get_current_user
router = APIRouter()
class CreateDocumentRequest(BaseModel):
title: str
type: str # "book", "chapter", "scene"
parent_id: Optional[str] = None
content: str = ""
class UpdateDocumentRequest(BaseModel):
title: Optional[str] = None
content: Optional[str] = None
parent_id: Optional[str] = None # Для переноса
order: Optional[int] = None
def _get_user_project(project_id: str, current_user: User) -> None:
"""Проверить что проект принадлежит пользователю"""
from services.project_service import project_service
project = project_service.get_project(project_id)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
if project.user_id != current_user.id:
raise HTTPException(status_code=403, detail="Access denied")
@router.get(
"/projects/{project_id}/documents/tree", response_model=List[Dict[str, Any]]
)
async def get_documents_tree(
project_id: str, current_user: User = Depends(get_current_user)
):
"""Получить документы в виде дерева"""
_get_user_project(project_id, current_user)
return document_service.get_documents_tree(project_id, current_user.id)
@router.get("/projects/{project_id}/documents", response_model=List[DocumentNode])
async def get_all_documents(
project_id: str, current_user: User = Depends(get_current_user)
):
"""Получить все документы (плоский список)"""
_get_user_project(project_id, current_user)
return document_service.get_all_documents(project_id, current_user.id)
@router.get(
"/projects/{project_id}/documents/{document_id}", response_model=DocumentNode
)
async def get_document(
project_id: str,
document_id: str,
current_user: User = Depends(get_current_user),
):
"""Получить документ по ID"""
_get_user_project(project_id, current_user)
document = document_service.get_document(project_id, document_id, current_user.id)
if not document:
raise HTTPException(status_code=404, detail="Document not found")
return document
@router.post(
"/projects/{project_id}/documents", response_model=DocumentNode, status_code=201
)
async def create_document(
project_id: str,
request: CreateDocumentRequest,
current_user: User = Depends(get_current_user),
):
"""Создать новый документ"""
_get_user_project(project_id, current_user)
try:
return document_service.create_document(
project_id=project_id,
title=request.title,
doc_type=request.type,
user_id=current_user.id,
parent_id=request.parent_id,
content=request.content,
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.put(
"/projects/{project_id}/documents/{document_id}", response_model=DocumentNode
)
async def update_document(
project_id: str,
document_id: str,
request: UpdateDocumentRequest,
current_user: User = Depends(get_current_user),
):
"""Обновить документ (включая перенос)"""
_get_user_project(project_id, current_user)
try:
document = document_service.update_document(
project_id=project_id,
document_id=document_id,
user_id=current_user.id,
title=request.title,
content=request.content,
parent_id=request.parent_id,
order=request.order,
)
if not document:
raise HTTPException(status_code=404, detail="Document not found")
return document
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.delete("/projects/{project_id}/documents/{document_id}")
async def delete_document(
project_id: str,
document_id: str,
current_user: User = Depends(get_current_user),
):
"""Удалить документ с каскадом"""
_get_user_project(project_id, current_user)
success = document_service.delete_document(project_id, document_id, current_user.id)
if not success:
raise HTTPException(status_code=404, detail="Document not found")
return {"status": "deleted", "document_id": document_id}
@router.get(
"/projects/{project_id}/documents/{parent_id}/children",
response_model=List[DocumentNode],
)
async def get_children(
project_id: str,
parent_id: str,
current_user: User = Depends(get_current_user),
):
"""Получить прямых детей узла"""
_get_user_project(project_id, current_user)
return document_service.get_children(project_id, parent_id, current_user.id)
+64
View File
@@ -0,0 +1,64 @@
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import Response
from models.user import User
from pydantic import BaseModel
from services.export_service import export_service
from routers.auth import get_current_user
router = APIRouter()
class ExportRequest(BaseModel):
project_id: str
document_id: Optional[str] = (
None # ID книги или главы. Если None — экспортируем всю книгу
)
format: str # 'epub', 'fb2', 'pdf', 'docx'
def _get_user_project(project_id: str, current_user: User) -> None:
"""Проверить что проект принадлежит пользователю"""
from services.project_service import project_service
project = project_service.get_project(project_id)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
if project.user_id != current_user.id:
raise HTTPException(status_code=403, detail="Access denied")
@router.post("/export")
async def export_book(
request: ExportRequest, current_user: User = Depends(get_current_user)
):
_get_user_project(request.project_id, current_user)
try:
file_content = export_service.export_document(
project_id=request.project_id,
user_id=current_user.id,
document_id=request.document_id,
format=request.format,
)
# Определяем MIME-тип
mime_types = {
"epub": "application/epub+zip",
"fb2": "application/fb2",
"pdf": "application/pdf",
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
}
filename = f"export.{request.format}"
return Response(
content=file_content,
media_type=mime_types.get(request.format, "application/octet-stream"),
headers={"Content-Disposition": f"attachment; filename={filename}"},
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
+8
View File
@@ -0,0 +1,8 @@
from fastapi import APIRouter
router = APIRouter()
@router.get("/health")
async def health_check():
return {"status": "ok", "message": "Writer IDE API is running"}
+40
View File
@@ -0,0 +1,40 @@
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import FileResponse
from models.user import User
from services.project_service import project_service
from routers.auth import get_current_user
router = APIRouter()
@router.get("/images/{project_id}/{filename}")
async def get_image(
project_id: str, filename: str, current_user: User = Depends(get_current_user)
):
"""Отдаёт изображение из папки проекта"""
try:
project_dir = project_service.get_project_dir(project_id, current_user.id)
image_path = project_dir / "images" / filename
if not image_path.exists():
raise HTTPException(status_code=404, detail="Image not found")
# Определяем MIME тип по расширению
suffix = Path(filename).suffix.lower()
mime_types = {
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".webp": "image/webp",
".gif": "image/gif",
}
return FileResponse(
path=image_path,
media_type=mime_types.get(suffix, "application/octet-stream"),
)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
+85
View File
@@ -0,0 +1,85 @@
from fastapi import APIRouter, Depends, HTTPException
from models.project import Project
from models.user import User
from pydantic import BaseModel
from services.project_service import project_service
from routers.auth import get_current_user
router = APIRouter()
class CreateProjectRequest(BaseModel):
title: str
description: str = ""
class UpdateProjectRequest(BaseModel):
title: str | None = None
description: str | None = None
@router.get("/projects", response_model=list[Project])
async def get_all_projects(current_user: User = Depends(get_current_user)):
"""Получить все проекты текущего пользователя"""
return project_service.get_all_projects(user_id=current_user.id)
@router.get("/projects/{project_id}", response_model=Project)
async def get_project(project_id: str, current_user: User = Depends(get_current_user)):
"""Получить проект по ID"""
project = project_service.get_project(project_id)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
if project.user_id != current_user.id:
raise HTTPException(status_code=403, detail="Access denied")
return project
@router.post("/projects", response_model=Project, status_code=201)
async def create_project(
request: CreateProjectRequest, current_user: User = Depends(get_current_user)
):
"""Создать новый проект"""
return project_service.create_project(
title=request.title,
user_id=current_user.id,
description=request.description,
)
@router.put("/projects/{project_id}", response_model=Project)
async def update_project(
project_id: str,
request: UpdateProjectRequest,
current_user: User = Depends(get_current_user),
):
"""Обновить проект"""
project = project_service.get_project(project_id)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
if project.user_id != current_user.id:
raise HTTPException(status_code=403, detail="Access denied")
updated = project_service.update_project(
project_id=project_id,
title=request.title,
description=request.description,
)
if not updated:
raise HTTPException(status_code=404, detail="Project not found")
return updated
@router.delete("/projects/{project_id}")
async def delete_project(
project_id: str, current_user: User = Depends(get_current_user)
):
"""Удалить проект"""
try:
success = project_service.delete_project(project_id, current_user.id)
if not success:
raise HTTPException(status_code=404, detail="Project not found")
return {"status": "deleted", "project_id": project_id}
except ValueError as e:
raise HTTPException(status_code=403, detail=str(e))