added RAG, Multiuser, TG bot
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.api.schemas import MessageOut
|
||||
|
||||
|
||||
class MessagesPageOut(BaseModel):
|
||||
messages: list[MessageOut]
|
||||
has_more: bool
|
||||
|
||||
|
||||
class GenerationStatusOut(BaseModel):
|
||||
active: bool
|
||||
@@ -1,17 +1,20 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.routes import character, chat, fitness, health, homelab, media, memory, pomodoro, projects, reminders, shopping, webhooks
|
||||
|
||||
api_router = APIRouter(prefix="/api/v1")
|
||||
api_router.include_router(health.router, tags=["health"])
|
||||
api_router.include_router(homelab.router, tags=["homelab"])
|
||||
api_router.include_router(chat.router, prefix="/chat", tags=["chat"])
|
||||
api_router.include_router(pomodoro.router, prefix="/pomodoro", tags=["pomodoro"])
|
||||
api_router.include_router(character.router, tags=["character"])
|
||||
api_router.include_router(projects.router, tags=["projects"])
|
||||
api_router.include_router(memory.router, tags=["memory"])
|
||||
api_router.include_router(fitness.router, tags=["fitness"])
|
||||
api_router.include_router(shopping.router, prefix="/shopping", tags=["shopping"])
|
||||
api_router.include_router(reminders.router, prefix="/reminders", tags=["reminders"])
|
||||
api_router.include_router(webhooks.router, tags=["webhooks"])
|
||||
api_router.include_router(media.router, tags=["media"])
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.routes import auth, character, chat, documents, fitness, health, homelab, media, memory, pomodoro, projects, reminders, settings, shopping, webhooks
|
||||
|
||||
api_router = APIRouter(prefix="/api/v1")
|
||||
api_router.include_router(health.router, tags=["health"])
|
||||
api_router.include_router(auth.router)
|
||||
api_router.include_router(homelab.router, tags=["homelab"])
|
||||
api_router.include_router(chat.router, prefix="/chat", tags=["chat"])
|
||||
api_router.include_router(pomodoro.router, prefix="/pomodoro", tags=["pomodoro"])
|
||||
api_router.include_router(character.router, tags=["character"])
|
||||
api_router.include_router(projects.router, tags=["projects"])
|
||||
api_router.include_router(memory.router, tags=["memory"])
|
||||
api_router.include_router(fitness.router, tags=["fitness"])
|
||||
api_router.include_router(shopping.router, prefix="/shopping", tags=["shopping"])
|
||||
api_router.include_router(reminders.router, prefix="/reminders", tags=["reminders"])
|
||||
api_router.include_router(webhooks.router, tags=["webhooks"])
|
||||
api_router.include_router(media.router, tags=["media"])
|
||||
api_router.include_router(settings.router, tags=["settings"])
|
||||
api_router.include_router(documents.router, tags=["documents"])
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.routes import character, chat, fitness, health, homelab, media, memory, pomodoro, projects, reminders, shopping, webhooks
|
||||
|
||||
api_router = APIRouter(prefix="/api/v1")
|
||||
api_router.include_router(health.router, tags=["health"])
|
||||
api_router.include_router(homelab.router, tags=["homelab"])
|
||||
api_router.include_router(chat.router, prefix="/chat", tags=["chat"])
|
||||
api_router.include_router(pomodoro.router, prefix="/pomodoro", tags=["pomodoro"])
|
||||
api_router.include_router(character.router, tags=["character"])
|
||||
api_router.include_router(projects.router, tags=["projects"])
|
||||
api_router.include_router(memory.router, tags=["memory"])
|
||||
api_router.include_router(fitness.router, tags=["fitness"])
|
||||
api_router.include_router(shopping.router, prefix="/shopping", tags=["shopping"])
|
||||
api_router.include_router(reminders.router, prefix="/reminders", tags=["reminders"])
|
||||
api_router.include_router(webhooks.router, tags=["webhooks"])
|
||||
api_router.include_router(media.router, tags=["media"])
|
||||
@@ -0,0 +1,73 @@
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.auth.deps import get_current_user
|
||||
from app.auth.service import create_user, find_user_by_token, user_to_dict
|
||||
from app.db.base import get_db
|
||||
from app.db.models import User
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
token: str = Field(min_length=8, max_length=256)
|
||||
|
||||
|
||||
class CreateUserRequest(BaseModel):
|
||||
username: str = Field(min_length=2, max_length=64)
|
||||
display_name: str = ""
|
||||
token: str | None = Field(default=None, min_length=8, max_length=256)
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
def login(payload: LoginRequest, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
user = find_user_by_token(db, payload.token)
|
||||
if not user:
|
||||
raise HTTPException(status_code=401, detail="Неверный токен")
|
||||
return {"ok": True, "user": user_to_dict(user), "token": payload.token.strip()}
|
||||
|
||||
|
||||
@router.get("/me")
|
||||
def me(user: User = Depends(get_current_user)) -> dict[str, Any]:
|
||||
return {"ok": True, "user": user_to_dict(user)}
|
||||
|
||||
|
||||
@router.get("/users")
|
||||
def list_users(
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
rows = db.scalars(select(User).where(User.is_active.is_(True)).order_by(User.id)).all()
|
||||
return {
|
||||
"ok": True,
|
||||
"users": [user_to_dict(row) for row in rows],
|
||||
"current_user_id": user.id,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/users")
|
||||
def register_user(
|
||||
payload: CreateUserRequest,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
new_user, plain_token = create_user(
|
||||
db,
|
||||
username=payload.username,
|
||||
display_name=payload.display_name,
|
||||
api_token=payload.token,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"user": user_to_dict(new_user),
|
||||
"token": plain_token,
|
||||
"created_by": user.username,
|
||||
}
|
||||
@@ -1,62 +1,80 @@
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.character.service import CharacterService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class CharacterCardData(BaseModel):
|
||||
name: str = "Ассистент"
|
||||
description: str = ""
|
||||
personality: str = ""
|
||||
scenario: str = ""
|
||||
first_mes: str = ""
|
||||
mes_example: str = ""
|
||||
system_prompt: str = ""
|
||||
post_history_instructions: str = ""
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
creator: str = ""
|
||||
creator_notes: str = ""
|
||||
alternate_greetings: list[str] = Field(default_factory=list)
|
||||
character_version: str = "1.0"
|
||||
appearance_tags: str = ""
|
||||
appearance_prose: str = ""
|
||||
lora_name: str = ""
|
||||
lora_weight: float = 0.8
|
||||
rp_persona_id: str = ""
|
||||
sd_enabled: bool = True
|
||||
|
||||
|
||||
class CharacterCardV2(BaseModel):
|
||||
spec: str = "chara_card_v2"
|
||||
spec_version: str = "2.0"
|
||||
data: CharacterCardData
|
||||
|
||||
|
||||
@router.get("/character")
|
||||
def get_character() -> dict[str, Any]:
|
||||
return CharacterService().get_card()
|
||||
|
||||
|
||||
@router.put("/character")
|
||||
def update_character(payload: CharacterCardV2) -> dict[str, Any]:
|
||||
return CharacterService().save_card(payload.model_dump())
|
||||
|
||||
|
||||
@router.get("/character/prompt")
|
||||
def get_character_prompt() -> dict[str, str]:
|
||||
service = CharacterService()
|
||||
return {
|
||||
"system_prompt": service.get_system_prompt(),
|
||||
"first_mes": service.get_card().get("data", {}).get("first_mes", ""),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/character/import")
|
||||
def import_character(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
if not payload:
|
||||
raise HTTPException(status_code=400, detail="Empty card")
|
||||
return CharacterService().save_card(payload)
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.auth.deps import get_current_user
|
||||
from app.character.service import CharacterService
|
||||
from app.db.base import get_db
|
||||
from app.db.models import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class CharacterCardData(BaseModel):
|
||||
name: str = "Ассистент"
|
||||
description: str = ""
|
||||
personality: str = ""
|
||||
scenario: str = ""
|
||||
first_mes: str = ""
|
||||
mes_example: str = ""
|
||||
system_prompt: str = ""
|
||||
post_history_instructions: str = ""
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
creator: str = ""
|
||||
creator_notes: str = ""
|
||||
alternate_greetings: list[str] = Field(default_factory=list)
|
||||
character_version: str = "1.0"
|
||||
appearance_tags: str = ""
|
||||
appearance_prose: str = ""
|
||||
lora_name: str = ""
|
||||
lora_weight: float = 0.8
|
||||
rp_persona_id: str = ""
|
||||
sd_enabled: bool = True
|
||||
|
||||
|
||||
class CharacterCardV2(BaseModel):
|
||||
spec: str = "chara_card_v2"
|
||||
spec_version: str = "2.0"
|
||||
data: CharacterCardData
|
||||
|
||||
|
||||
@router.get("/character")
|
||||
def get_character(
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
return CharacterService(db, user.id).get_card()
|
||||
|
||||
|
||||
@router.put("/character")
|
||||
def update_character(
|
||||
payload: CharacterCardV2,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
return CharacterService(db, user.id).save_card(payload.model_dump())
|
||||
|
||||
|
||||
@router.get("/character/prompt")
|
||||
def get_character_prompt(
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
) -> dict[str, str]:
|
||||
service = CharacterService(db, user.id)
|
||||
return {
|
||||
"system_prompt": service.get_system_prompt(),
|
||||
"first_mes": service.get_card().get("data", {}).get("first_mes", ""),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/character/import")
|
||||
def import_character(
|
||||
payload: dict[str, Any],
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
if not payload:
|
||||
raise HTTPException(status_code=400, detail="Empty card")
|
||||
return CharacterService(db, user.id).save_card(payload)
|
||||
|
||||
+158
-70
@@ -1,70 +1,158 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.schemas import MessageCreate, MessageOut, SessionCreate, SessionDetailOut, SessionOut
|
||||
from app.chat.service import ChatService
|
||||
from app.db.base import get_db
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/sessions", response_model=SessionOut)
|
||||
def create_session(payload: SessionCreate, db: Session = Depends(get_db)) -> SessionOut:
|
||||
service = ChatService(db)
|
||||
return service.create_session(title=payload.title)
|
||||
|
||||
|
||||
@router.get("/sessions", response_model=list[SessionOut])
|
||||
def list_sessions(db: Session = Depends(get_db)) -> list[SessionOut]:
|
||||
service = ChatService(db)
|
||||
return service.list_sessions()
|
||||
|
||||
|
||||
@router.get("/sessions/{session_id}", response_model=SessionDetailOut)
|
||||
def get_session(session_id: int, db: Session = Depends(get_db)) -> SessionDetailOut:
|
||||
service = ChatService(db)
|
||||
session = service.get_session(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
return session
|
||||
|
||||
|
||||
@router.delete("/sessions/{session_id}")
|
||||
def delete_session(session_id: int, db: Session = Depends(get_db)) -> dict[str, bool]:
|
||||
service = ChatService(db)
|
||||
if not service.delete_session(session_id):
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.post("/sessions/{session_id}/messages")
|
||||
async def send_message(
|
||||
session_id: int,
|
||||
payload: MessageCreate,
|
||||
db: Session = Depends(get_db),
|
||||
) -> StreamingResponse:
|
||||
service = ChatService(db)
|
||||
if not service.get_session(session_id):
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
|
||||
# Сохраняем user до стрима: иначе при обрыве SSE сообщение не попадает в БД.
|
||||
service.save_user_message(session_id, payload.content)
|
||||
|
||||
async def event_stream():
|
||||
async for chunk in service.stream_response(
|
||||
session_id,
|
||||
payload.content,
|
||||
user_message_saved=True,
|
||||
):
|
||||
yield chunk
|
||||
|
||||
return StreamingResponse(
|
||||
event_stream(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
)
|
||||
import asyncio
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.chat_schemas import GenerationStatusOut, MessagesPageOut
|
||||
from app.api.schemas import (
|
||||
MessageCreate,
|
||||
SessionCreate,
|
||||
SessionDetailOut,
|
||||
SessionOut,
|
||||
)
|
||||
from app.chat.generation import (
|
||||
GenerationBusyError,
|
||||
get_active_handle,
|
||||
is_generation_active,
|
||||
start_generation,
|
||||
subscribe_generation,
|
||||
)
|
||||
from app.chat.service import ChatService
|
||||
from app.auth.deps import get_current_user
|
||||
from app.db.base import get_db
|
||||
from app.db.models import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/sessions", response_model=SessionOut)
|
||||
def create_session(payload: SessionCreate, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> SessionOut:
|
||||
service = ChatService(db, user.id)
|
||||
return service.create_session(title=payload.title)
|
||||
|
||||
|
||||
@router.get("/sessions", response_model=list[SessionOut])
|
||||
def list_sessions(db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> list[SessionOut]:
|
||||
service = ChatService(db, user.id)
|
||||
return service.list_sessions()
|
||||
|
||||
|
||||
@router.get("/sessions/{session_id}", response_model=SessionDetailOut)
|
||||
def get_session(session_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> SessionDetailOut:
|
||||
service = ChatService(db, user.id)
|
||||
session = service.get_session(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
return session
|
||||
|
||||
|
||||
@router.get("/sessions/{session_id}/messages", response_model=MessagesPageOut)
|
||||
def list_messages(
|
||||
session_id: int,
|
||||
limit: int = 30,
|
||||
before_id: int | None = None,
|
||||
after_id: int | None = None,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> MessagesPageOut:
|
||||
service = ChatService(db, user.id)
|
||||
if not service.get_session(session_id):
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
messages, has_more = service.list_messages(
|
||||
session_id,
|
||||
limit=min(max(limit, 1), 100),
|
||||
before_id=before_id,
|
||||
after_id=after_id,
|
||||
)
|
||||
return MessagesPageOut(messages=messages, has_more=has_more)
|
||||
|
||||
|
||||
@router.get("/sessions/{session_id}/generation", response_model=GenerationStatusOut)
|
||||
def generation_status(session_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> GenerationStatusOut:
|
||||
service = ChatService(db, user.id)
|
||||
if not service.get_session(session_id):
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
return GenerationStatusOut(active=is_generation_active(session_id))
|
||||
|
||||
|
||||
@router.get("/sessions/{session_id}/generation/stream")
|
||||
async def generation_stream(session_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> StreamingResponse:
|
||||
service = ChatService(db, user.id)
|
||||
if not service.get_session(session_id):
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
|
||||
handle = get_active_handle(session_id)
|
||||
if not handle:
|
||||
raise HTTPException(status_code=404, detail="No active generation")
|
||||
|
||||
async def event_stream():
|
||||
async for chunk in subscribe_generation(handle):
|
||||
yield chunk
|
||||
|
||||
return StreamingResponse(
|
||||
event_stream(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/sessions/{session_id}")
|
||||
def delete_session(session_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, bool]:
|
||||
service = ChatService(db, user.id)
|
||||
if not service.delete_session(session_id):
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.post("/sessions/{session_id}/messages")
|
||||
async def send_message(
|
||||
session_id: int,
|
||||
payload: MessageCreate,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> StreamingResponse:
|
||||
service = ChatService(db, user.id)
|
||||
if not service.get_session(session_id):
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
|
||||
if is_generation_active(session_id):
|
||||
raise HTTPException(status_code=409, detail="Generation already in progress")
|
||||
|
||||
# Сохраняем user до стрима: иначе при обрыве SSE сообщение не попадает в БД.
|
||||
service.save_user_message(session_id, payload.content)
|
||||
|
||||
try:
|
||||
handle = await start_generation(session_id, user.id, payload.content)
|
||||
except GenerationBusyError:
|
||||
raise HTTPException(status_code=409, detail="Generation already in progress") from None
|
||||
|
||||
async def event_stream():
|
||||
try:
|
||||
async for chunk in subscribe_generation(handle):
|
||||
yield chunk
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
|
||||
return StreamingResponse(
|
||||
event_stream(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/sessions/{session_id}/context-preview")
|
||||
def context_preview(
|
||||
session_id: int,
|
||||
query: str | None = None,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> dict:
|
||||
service = ChatService(db, user.id)
|
||||
return service.context_preview(session_id, query=query)
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.schemas import MessageCreate, MessageOut, SessionCreate, SessionDetailOut, SessionOut
|
||||
from app.chat.service import ChatService
|
||||
from app.db.base import get_db
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/sessions", response_model=SessionOut)
|
||||
def create_session(payload: SessionCreate, db: Session = Depends(get_db)) -> SessionOut:
|
||||
service = ChatService(db)
|
||||
return service.create_session(title=payload.title)
|
||||
|
||||
|
||||
@router.get("/sessions", response_model=list[SessionOut])
|
||||
def list_sessions(db: Session = Depends(get_db)) -> list[SessionOut]:
|
||||
service = ChatService(db)
|
||||
return service.list_sessions()
|
||||
|
||||
|
||||
@router.get("/sessions/{session_id}", response_model=SessionDetailOut)
|
||||
def get_session(session_id: int, db: Session = Depends(get_db)) -> SessionDetailOut:
|
||||
service = ChatService(db)
|
||||
session = service.get_session(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
return session
|
||||
|
||||
|
||||
@router.delete("/sessions/{session_id}")
|
||||
def delete_session(session_id: int, db: Session = Depends(get_db)) -> dict[str, bool]:
|
||||
service = ChatService(db)
|
||||
if not service.delete_session(session_id):
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.post("/sessions/{session_id}/messages")
|
||||
async def send_message(
|
||||
session_id: int,
|
||||
payload: MessageCreate,
|
||||
db: Session = Depends(get_db),
|
||||
) -> StreamingResponse:
|
||||
service = ChatService(db)
|
||||
if not service.get_session(session_id):
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
|
||||
# Сохраняем user до стрима: иначе при обрыве SSE сообщение не попадает в БД.
|
||||
service.save_user_message(session_id, payload.content)
|
||||
|
||||
async def event_stream():
|
||||
async for chunk in service.stream_response(
|
||||
session_id,
|
||||
payload.content,
|
||||
user_message_saved=True,
|
||||
):
|
||||
yield chunk
|
||||
|
||||
return StreamingResponse(
|
||||
event_stream(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,51 @@
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.auth.deps import get_current_user
|
||||
from app.db.base import get_db
|
||||
from app.db.models import User
|
||||
from app.db.models import Document
|
||||
from app.rag.ingest import ingest_document_file
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/documents")
|
||||
def list_documents(db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> list[dict[str, Any]]:
|
||||
docs = db.scalars(select(Document).where(Document.user_id == user.id).order_by(Document.created_at.desc())).all()
|
||||
return [
|
||||
{
|
||||
"id": d.id,
|
||||
"title": d.title,
|
||||
"filename": d.filename,
|
||||
"size_bytes": d.size_bytes,
|
||||
"created_at": d.created_at.isoformat() if d.created_at else None,
|
||||
}
|
||||
for d in docs
|
||||
]
|
||||
|
||||
|
||||
@router.post("/documents/upload")
|
||||
async def upload_document(
|
||||
file: UploadFile = File(...),
|
||||
title: str = Form(""),
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
raw = await file.read()
|
||||
if not raw:
|
||||
raise HTTPException(status_code=400, detail="Empty file")
|
||||
try:
|
||||
doc = await ingest_document_file(
|
||||
db,
|
||||
user_id=user.id,
|
||||
title=title.strip() or (file.filename or "document"),
|
||||
filename=file.filename or "upload.txt",
|
||||
raw_bytes=raw,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
return {"ok": True, "document": doc}
|
||||
+308
-223
@@ -1,223 +1,308 @@
|
||||
from datetime import date
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.base import get_db
|
||||
from app.fitness.service import FitnessService
|
||||
from app.fitness.structuring import structure_meal, structure_workout
|
||||
from app.integrations.openfoodfacts import OpenFoodFactsClient
|
||||
from app.integrations.wger import WgerClient
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ProfileUpdate(BaseModel):
|
||||
sex: str | None = None
|
||||
age: int | None = None
|
||||
height_cm: float | None = None
|
||||
weight_kg: float | None = None
|
||||
activity_level: str | None = None
|
||||
goal: str | None = None
|
||||
target_weight_kg: float | None = None
|
||||
weekly_workouts: int | None = None
|
||||
|
||||
|
||||
class MealCreate(BaseModel):
|
||||
text: str = Field(min_length=1)
|
||||
meal_type: str | None = None
|
||||
|
||||
|
||||
class WaterCreate(BaseModel):
|
||||
amount_ml: int = Field(gt=0)
|
||||
|
||||
|
||||
class WeightCreate(BaseModel):
|
||||
weight_kg: float = Field(gt=0)
|
||||
body_fat_pct: float | None = None
|
||||
chest_cm: float | None = None
|
||||
waist_cm: float | None = None
|
||||
notes: str = ""
|
||||
|
||||
|
||||
class WorkoutCreate(BaseModel):
|
||||
text: str = Field(min_length=1)
|
||||
|
||||
|
||||
class ReminderUpdate(BaseModel):
|
||||
enabled: bool | None = None
|
||||
hour: int | None = Field(default=None, ge=0, le=23)
|
||||
minute: int | None = Field(default=None, ge=0, le=59)
|
||||
interval_hours: int | None = Field(default=None, ge=1, le=12)
|
||||
|
||||
|
||||
@router.get("/fitness")
|
||||
def get_snapshot(db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
return FitnessService(db).snapshot()
|
||||
|
||||
|
||||
@router.get("/fitness/summary")
|
||||
def get_summary(
|
||||
day: str | None = None,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
d = date.fromisoformat(day) if day else None
|
||||
return FitnessService(db).get_daily_summary(d)
|
||||
|
||||
|
||||
@router.get("/fitness/history")
|
||||
def get_history(
|
||||
days: int = 7,
|
||||
end: str | None = None,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
end_day = date.fromisoformat(end) if end else None
|
||||
return FitnessService(db).get_history(days=days, end_day=end_day)
|
||||
|
||||
|
||||
@router.get("/fitness/profile")
|
||||
def get_profile(db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
profile = FitnessService(db).get_profile()
|
||||
return profile or {"configured": False}
|
||||
|
||||
|
||||
@router.put("/fitness/profile")
|
||||
def update_profile(
|
||||
payload: ProfileUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
return FitnessService(db).set_profile(payload.model_dump(exclude_none=True))
|
||||
|
||||
|
||||
@router.post("/fitness/profile/calc")
|
||||
def calc_targets(
|
||||
payload: ProfileUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
params = payload.model_dump(exclude_none=True)
|
||||
if not params:
|
||||
raise HTTPException(status_code=400, detail="No parameters")
|
||||
return FitnessService(db).calc_targets(params)
|
||||
|
||||
|
||||
@router.post("/fitness/meals")
|
||||
async def create_meal(
|
||||
payload: MealCreate,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
service = FitnessService(db)
|
||||
try:
|
||||
structured = await structure_meal(payload.text)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
return service.log_meal(
|
||||
description=structured.get("description") or payload.text,
|
||||
meal_type=payload.meal_type or structured.get("meal_type") or "snack",
|
||||
calories=float(structured.get("calories") or 0),
|
||||
protein_g=float(structured.get("protein_g") or 0),
|
||||
fat_g=float(structured.get("fat_g") or 0),
|
||||
carbs_g=float(structured.get("carbs_g") or 0),
|
||||
source="llm",
|
||||
estimated=bool(structured.get("estimated", True)),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/fitness/water")
|
||||
def create_water(
|
||||
payload: WaterCreate,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
return FitnessService(db).log_water(payload.amount_ml)
|
||||
|
||||
|
||||
@router.post("/fitness/weight")
|
||||
def create_weight(
|
||||
payload: WeightCreate,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
return FitnessService(db).log_weight(
|
||||
payload.weight_kg,
|
||||
body_fat_pct=payload.body_fat_pct,
|
||||
chest_cm=payload.chest_cm,
|
||||
waist_cm=payload.waist_cm,
|
||||
notes=payload.notes,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/fitness/workouts")
|
||||
async def create_workout(
|
||||
payload: WorkoutCreate,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
service = FitnessService(db)
|
||||
try:
|
||||
structured = await structure_workout(payload.text)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
return service.log_workout(
|
||||
title=structured.get("title") or "Тренировка",
|
||||
notes=structured.get("notes") or payload.text,
|
||||
duration_min=structured.get("duration_min"),
|
||||
exercises=structured.get("exercises"),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/fitness/body-metrics")
|
||||
def list_metrics(
|
||||
limit: int = 30,
|
||||
db: Session = Depends(get_db),
|
||||
) -> list[dict[str, Any]]:
|
||||
return FitnessService(db).list_body_metrics(limit=limit)
|
||||
|
||||
|
||||
@router.delete("/fitness/meals/{log_id}")
|
||||
def delete_meal(log_id: int, db: Session = Depends(get_db)) -> dict[str, bool]:
|
||||
if not FitnessService(db).delete_food_log(log_id):
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.delete("/fitness/water/{log_id}")
|
||||
def delete_water(log_id: int, db: Session = Depends(get_db)) -> dict[str, bool]:
|
||||
if not FitnessService(db).delete_water_log(log_id):
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.delete("/fitness/workouts/{log_id}")
|
||||
def delete_workout(log_id: int, db: Session = Depends(get_db)) -> dict[str, bool]:
|
||||
if not FitnessService(db).delete_workout_log(log_id):
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.get("/fitness/reminders")
|
||||
def list_reminders(db: Session = Depends(get_db)) -> list[dict[str, Any]]:
|
||||
return FitnessService(db).list_reminders()
|
||||
|
||||
|
||||
@router.put("/fitness/reminders/{kind}")
|
||||
def update_reminder(
|
||||
kind: str,
|
||||
payload: ReminderUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
return FitnessService(db).set_reminder(
|
||||
kind,
|
||||
enabled=payload.enabled,
|
||||
hour=payload.hour,
|
||||
minute=payload.minute,
|
||||
interval_hours=payload.interval_hours,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/fitness/lookup/food")
|
||||
def lookup_food(q: str, limit: int = 5) -> list[dict[str, Any]]:
|
||||
return OpenFoodFactsClient().search(q, limit=limit)
|
||||
|
||||
|
||||
@router.get("/fitness/lookup/exercise")
|
||||
def lookup_exercise(q: str, limit: int = 8) -> list[dict[str, Any]]:
|
||||
return WgerClient().search_exercises(q, limit=limit)
|
||||
from datetime import date
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.auth.deps import get_current_user
|
||||
from app.db.base import get_db
|
||||
from app.db.models import User
|
||||
from app.fitness.service import FitnessService
|
||||
from app.fitness.structuring import structure_meal, structure_workout
|
||||
from app.integrations.openfoodfacts import OpenFoodFactsClient
|
||||
from app.integrations.wger import WgerClient
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ProfileUpdate(BaseModel):
|
||||
sex: str | None = None
|
||||
age: int | None = None
|
||||
height_cm: float | None = None
|
||||
weight_kg: float | None = None
|
||||
activity_level: str | None = None
|
||||
goal: str | None = None
|
||||
target_weight_kg: float | None = None
|
||||
weekly_workouts: int | None = None
|
||||
baseline_steps: int | None = None
|
||||
baseline_workout_kcal: float | None = None
|
||||
|
||||
|
||||
class MealCreate(BaseModel):
|
||||
text: str = Field(min_length=1)
|
||||
meal_type: str | None = None
|
||||
|
||||
|
||||
class WaterCreate(BaseModel):
|
||||
amount_ml: int = Field(gt=0)
|
||||
|
||||
|
||||
class WeightCreate(BaseModel):
|
||||
weight_kg: float = Field(gt=0)
|
||||
body_fat_pct: float | None = None
|
||||
chest_cm: float | None = None
|
||||
waist_cm: float | None = None
|
||||
neck_cm: float | None = None
|
||||
hip_cm: float | None = None
|
||||
notes: str = ""
|
||||
day: str | None = None
|
||||
days_ago: int | None = Field(default=None, ge=0, le=90)
|
||||
recorded_at: str | None = None
|
||||
|
||||
|
||||
class BodyCompositionCalc(BaseModel):
|
||||
weight_kg: float | None = None
|
||||
height_cm: float | None = None
|
||||
sex: str | None = None
|
||||
neck_cm: float | None = None
|
||||
waist_cm: float | None = None
|
||||
hip_cm: float | None = None
|
||||
body_fat_pct: float | None = None
|
||||
|
||||
|
||||
class StepsCreate(BaseModel):
|
||||
steps: int = Field(ge=0)
|
||||
active_calories: float | None = None
|
||||
notes: str = ""
|
||||
day: str | None = None
|
||||
days_ago: int | None = Field(default=None, ge=0, le=90)
|
||||
logged_at: str | None = None
|
||||
|
||||
|
||||
class WorkoutCreate(BaseModel):
|
||||
text: str = Field(min_length=1)
|
||||
day: str | None = None
|
||||
days_ago: int | None = Field(default=None, ge=0, le=90)
|
||||
logged_at: str | None = None
|
||||
|
||||
|
||||
class ReminderUpdate(BaseModel):
|
||||
enabled: bool | None = None
|
||||
hour: int | None = Field(default=None, ge=0, le=23)
|
||||
minute: int | None = Field(default=None, ge=0, le=59)
|
||||
interval_hours: int | None = Field(default=None, ge=1, le=12)
|
||||
|
||||
|
||||
@router.get("/fitness")
|
||||
def get_snapshot(db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, Any]:
|
||||
return FitnessService(db, user.id).snapshot()
|
||||
|
||||
|
||||
@router.get("/fitness/summary")
|
||||
def get_summary(
|
||||
day: str | None = None,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
d = date.fromisoformat(day) if day else None
|
||||
return FitnessService(db, user.id).get_daily_summary(d)
|
||||
|
||||
|
||||
@router.get("/fitness/workout-stats")
|
||||
def get_workout_stats(
|
||||
days: int = 7,
|
||||
end: str | None = None,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
end_day = date.fromisoformat(end) if end else None
|
||||
return FitnessService(db, user.id).get_workout_stats(days=days, end_day=end_day)
|
||||
|
||||
|
||||
@router.get("/fitness/history")
|
||||
def get_history(
|
||||
days: int = 7,
|
||||
end: str | None = None,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
end_day = date.fromisoformat(end) if end else None
|
||||
return FitnessService(db, user.id).get_history(days=days, end_day=end_day)
|
||||
|
||||
|
||||
@router.get("/fitness/profile")
|
||||
def get_profile(db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, Any]:
|
||||
profile = FitnessService(db, user.id).get_profile()
|
||||
return profile or {"configured": False}
|
||||
|
||||
|
||||
@router.put("/fitness/profile")
|
||||
def update_profile(
|
||||
payload: ProfileUpdate,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
return FitnessService(db, user.id).set_profile(payload.model_dump(exclude_none=True))
|
||||
|
||||
|
||||
@router.post("/fitness/profile/calc")
|
||||
def calc_targets(
|
||||
payload: ProfileUpdate,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
params = payload.model_dump(exclude_none=True)
|
||||
if not params:
|
||||
raise HTTPException(status_code=400, detail="No parameters")
|
||||
return FitnessService(db, user.id).calc_targets(params)
|
||||
|
||||
|
||||
@router.post("/fitness/meals")
|
||||
async def create_meal(
|
||||
payload: MealCreate,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
service = FitnessService(db, user.id)
|
||||
try:
|
||||
structured = await structure_meal(payload.text)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
return service.log_meal(
|
||||
description=structured.get("description") or payload.text,
|
||||
meal_type=payload.meal_type or structured.get("meal_type") or "snack",
|
||||
calories=float(structured.get("calories") or 0),
|
||||
protein_g=float(structured.get("protein_g") or 0),
|
||||
fat_g=float(structured.get("fat_g") or 0),
|
||||
carbs_g=float(structured.get("carbs_g") or 0),
|
||||
source="llm",
|
||||
estimated=bool(structured.get("estimated", True)),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/fitness/water")
|
||||
def create_water(
|
||||
payload: WaterCreate,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
return FitnessService(db, user.id).log_water(payload.amount_ml)
|
||||
|
||||
|
||||
@router.post("/fitness/weight")
|
||||
def create_weight(
|
||||
payload: WeightCreate,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
day = date.fromisoformat(payload.day) if payload.day else None
|
||||
return FitnessService(db, user.id).log_weight(
|
||||
payload.weight_kg,
|
||||
body_fat_pct=payload.body_fat_pct,
|
||||
chest_cm=payload.chest_cm,
|
||||
waist_cm=payload.waist_cm,
|
||||
neck_cm=payload.neck_cm,
|
||||
hip_cm=payload.hip_cm,
|
||||
notes=payload.notes,
|
||||
recorded_at=payload.recorded_at,
|
||||
day=day,
|
||||
days_ago=payload.days_ago,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/fitness/body-composition/calc")
|
||||
def calc_body_composition(
|
||||
payload: BodyCompositionCalc,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
return FitnessService(db, user.id).calc_body_composition(payload.model_dump(exclude_none=True))
|
||||
|
||||
|
||||
@router.post("/fitness/steps")
|
||||
def create_steps(
|
||||
payload: StepsCreate,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
day = date.fromisoformat(payload.day) if payload.day else None
|
||||
return FitnessService(db, user.id).log_steps(
|
||||
payload.steps,
|
||||
active_calories=payload.active_calories,
|
||||
notes=payload.notes,
|
||||
day=day,
|
||||
days_ago=payload.days_ago,
|
||||
logged_at=payload.logged_at,
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/fitness/steps/{log_id}")
|
||||
def delete_steps(log_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, bool]:
|
||||
if not FitnessService(db, user.id).delete_step_log(log_id):
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.post("/fitness/workouts")
|
||||
async def create_workout(
|
||||
payload: WorkoutCreate,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
service = FitnessService(db, user.id)
|
||||
try:
|
||||
structured = await structure_workout(payload.text)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
day = date.fromisoformat(payload.day) if payload.day else None
|
||||
return service.log_workout(
|
||||
title=structured.get("title") or "Тренировка",
|
||||
notes=structured.get("notes") or payload.text,
|
||||
duration_min=structured.get("duration_min"),
|
||||
exercises=structured.get("exercises"),
|
||||
active_calories=structured.get("active_calories"),
|
||||
total_calories=structured.get("total_calories"),
|
||||
steps=structured.get("steps"),
|
||||
day=day,
|
||||
days_ago=payload.days_ago,
|
||||
logged_at=payload.logged_at,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/fitness/body-metrics")
|
||||
def list_metrics(
|
||||
limit: int = 30,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> list[dict[str, Any]]:
|
||||
return FitnessService(db, user.id).list_body_metrics(limit=limit)
|
||||
|
||||
|
||||
@router.delete("/fitness/meals/{log_id}")
|
||||
def delete_meal(log_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, bool]:
|
||||
if not FitnessService(db, user.id).delete_food_log(log_id):
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.delete("/fitness/water/{log_id}")
|
||||
def delete_water(log_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, bool]:
|
||||
if not FitnessService(db, user.id).delete_water_log(log_id):
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.delete("/fitness/workouts/{log_id}")
|
||||
def delete_workout(log_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, bool]:
|
||||
if not FitnessService(db, user.id).delete_workout_log(log_id):
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.get("/fitness/reminders")
|
||||
def list_reminders(db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> list[dict[str, Any]]:
|
||||
return FitnessService(db, user.id).list_reminders()
|
||||
|
||||
|
||||
@router.put("/fitness/reminders/{kind}")
|
||||
def update_reminder(
|
||||
kind: str,
|
||||
payload: ReminderUpdate,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
return FitnessService(db, user.id).set_reminder(
|
||||
kind,
|
||||
enabled=payload.enabled,
|
||||
hour=payload.hour,
|
||||
minute=payload.minute,
|
||||
interval_hours=payload.interval_hours,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/fitness/lookup/food")
|
||||
def lookup_food(q: str, limit: int = 5) -> list[dict[str, Any]]:
|
||||
return OpenFoodFactsClient().search(q, limit=limit)
|
||||
|
||||
|
||||
@router.get("/fitness/lookup/exercise")
|
||||
def lookup_exercise(q: str, limit: int = 8) -> list[dict[str, Any]]:
|
||||
return WgerClient().search_exercises(q, limit=limit)
|
||||
|
||||
+130
-127
@@ -1,127 +1,130 @@
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.base import get_db
|
||||
from app.db.models import ChatSession
|
||||
from app.memory.extract import extract_after_turn
|
||||
from app.memory.service import MemoryService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ProfileUpdate(BaseModel):
|
||||
updates: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class FactCreate(BaseModel):
|
||||
content: str = Field(min_length=1)
|
||||
category: str = "fact"
|
||||
importance: int = Field(default=3, ge=1, le=5)
|
||||
session_id: int | None = None
|
||||
|
||||
|
||||
class SessionSummaryUpdate(BaseModel):
|
||||
summary: str = Field(min_length=1)
|
||||
message_count: int = 0
|
||||
|
||||
|
||||
class ExtractRequest(BaseModel):
|
||||
session_id: int
|
||||
user_text: str = Field(min_length=1)
|
||||
assistant_text: str = ""
|
||||
force: bool = False
|
||||
|
||||
|
||||
@router.get("/memory")
|
||||
def get_memory_snapshot(
|
||||
session_id: int | None = None,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
return MemoryService(db).snapshot(session_id)
|
||||
|
||||
|
||||
@router.get("/profile")
|
||||
def get_profile(db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
return MemoryService(db).get_profile()
|
||||
|
||||
|
||||
@router.put("/profile")
|
||||
def update_profile(
|
||||
payload: ProfileUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return MemoryService(db).update_profile(payload.updates)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/memory/facts")
|
||||
def list_facts(
|
||||
query: str | None = None,
|
||||
category: str | None = None,
|
||||
limit: int = 30,
|
||||
db: Session = Depends(get_db),
|
||||
) -> list[dict[str, Any]]:
|
||||
return MemoryService(db).recall_memories(query=query, category=category, limit=limit)
|
||||
|
||||
|
||||
@router.post("/memory/facts")
|
||||
def create_fact(
|
||||
payload: FactCreate,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return MemoryService(db).remember_fact(
|
||||
payload.content,
|
||||
category=payload.category,
|
||||
session_id=payload.session_id,
|
||||
importance=payload.importance,
|
||||
source="api",
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.delete("/memory/facts/{memory_id}")
|
||||
def forget_fact(memory_id: int, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
try:
|
||||
return MemoryService(db).forget_memory(memory_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/memory/extract")
|
||||
async def extract_memories(
|
||||
payload: ExtractRequest,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
session = db.get(ChatSession, payload.session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
return await extract_after_turn(
|
||||
db,
|
||||
payload.session_id,
|
||||
payload.user_text,
|
||||
payload.assistant_text,
|
||||
force=payload.force,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/memory/sessions/{session_id}/summary")
|
||||
def update_session_summary(
|
||||
session_id: int,
|
||||
payload: SessionSummaryUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return MemoryService(db).update_session_summary(
|
||||
session_id,
|
||||
payload.summary,
|
||||
message_count=payload.message_count,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.auth.deps import get_current_user
|
||||
from app.db.base import get_db
|
||||
from app.db.models import User
|
||||
from app.db.models import ChatSession
|
||||
from app.memory.extract import extract_after_turn
|
||||
from app.memory.service import MemoryService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ProfileUpdate(BaseModel):
|
||||
updates: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class FactCreate(BaseModel):
|
||||
content: str = Field(min_length=1)
|
||||
category: str = "fact"
|
||||
importance: int = Field(default=3, ge=1, le=5)
|
||||
session_id: int | None = None
|
||||
|
||||
|
||||
class SessionSummaryUpdate(BaseModel):
|
||||
summary: str = Field(min_length=1)
|
||||
message_count: int = 0
|
||||
|
||||
|
||||
class ExtractRequest(BaseModel):
|
||||
session_id: int
|
||||
user_text: str = Field(min_length=1)
|
||||
assistant_text: str = ""
|
||||
force: bool = False
|
||||
|
||||
|
||||
@router.get("/memory")
|
||||
def get_memory_snapshot(
|
||||
session_id: int | None = None,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
return MemoryService(db, user.id).snapshot(session_id)
|
||||
|
||||
|
||||
@router.get("/profile")
|
||||
def get_profile(db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, Any]:
|
||||
return MemoryService(db, user.id).get_profile()
|
||||
|
||||
|
||||
@router.put("/profile")
|
||||
def update_profile(
|
||||
payload: ProfileUpdate,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return MemoryService(db, user.id).update_profile(payload.updates)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/memory/facts")
|
||||
def list_facts(
|
||||
query: str | None = None,
|
||||
category: str | None = None,
|
||||
limit: int = 30,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> list[dict[str, Any]]:
|
||||
return MemoryService(db, user.id).recall_memories(query=query, category=category, limit=limit)
|
||||
|
||||
|
||||
@router.post("/memory/facts")
|
||||
def create_fact(
|
||||
payload: FactCreate,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return MemoryService(db, user.id).remember_fact(
|
||||
payload.content,
|
||||
category=payload.category,
|
||||
session_id=payload.session_id,
|
||||
importance=payload.importance,
|
||||
source="api",
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.delete("/memory/facts/{memory_id}")
|
||||
def forget_fact(memory_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, Any]:
|
||||
try:
|
||||
return MemoryService(db, user.id).forget_memory(memory_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/memory/extract")
|
||||
async def extract_memories(
|
||||
payload: ExtractRequest,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> dict:
|
||||
session = db.get(ChatSession, payload.session_id)
|
||||
if not session or session.user_id != user.id:
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
return await extract_after_turn(
|
||||
db,
|
||||
payload.session_id,
|
||||
payload.user_text,
|
||||
payload.assistant_text,
|
||||
user_id=user.id,
|
||||
force=payload.force,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/memory/sessions/{session_id}/summary")
|
||||
def update_session_summary(
|
||||
session_id: int,
|
||||
payload: SessionSummaryUpdate,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return MemoryService(db, user.id).update_session_summary(
|
||||
session_id,
|
||||
payload.summary,
|
||||
message_count=payload.message_count,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
+102
-100
@@ -1,100 +1,102 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.schemas import PomodoroStart, PomodoroStop
|
||||
from app.db.base import get_db
|
||||
from app.pomodoro.service import PomodoroService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _handle_value_error(exc: ValueError) -> HTTPException:
|
||||
return HTTPException(status_code=400, detail=str(exc))
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
def get_status(db: Session = Depends(get_db)) -> dict:
|
||||
return PomodoroService(db).get_status()
|
||||
|
||||
|
||||
@router.post("/start")
|
||||
def start_pomodoro(payload: PomodoroStart, db: Session = Depends(get_db)) -> dict:
|
||||
try:
|
||||
return PomodoroService(db).start(
|
||||
duration_min=payload.duration_min,
|
||||
task_note=payload.task_note,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise _handle_value_error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/pause")
|
||||
def pause_pomodoro(db: Session = Depends(get_db)) -> dict:
|
||||
try:
|
||||
return PomodoroService(db).pause()
|
||||
except ValueError as exc:
|
||||
raise _handle_value_error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/resume")
|
||||
def resume_pomodoro(db: Session = Depends(get_db)) -> dict:
|
||||
try:
|
||||
return PomodoroService(db).resume()
|
||||
except ValueError as exc:
|
||||
raise _handle_value_error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/stop")
|
||||
def stop_pomodoro(payload: PomodoroStop, db: Session = Depends(get_db)) -> dict:
|
||||
try:
|
||||
return PomodoroService(db).stop(
|
||||
result=payload.result,
|
||||
completed=payload.completed,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise _handle_value_error(exc) from exc
|
||||
|
||||
|
||||
@router.get("/history")
|
||||
def get_history(limit: int = 20, db: Session = Depends(get_db)) -> list[dict]:
|
||||
return PomodoroService(db).history(limit=limit)
|
||||
|
||||
|
||||
@router.post("/work/start")
|
||||
def start_work(payload: PomodoroStart, db: Session = Depends(get_db)) -> dict:
|
||||
try:
|
||||
return PomodoroService(db).start_work(
|
||||
duration_min=payload.duration_min,
|
||||
task_note=payload.task_note,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise _handle_value_error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/break/short/start")
|
||||
def start_short_break(duration_min: int | None = None, db: Session = Depends(get_db)) -> dict:
|
||||
try:
|
||||
return PomodoroService(db).start_short_break(duration_min=duration_min)
|
||||
except ValueError as exc:
|
||||
raise _handle_value_error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/break/long/start")
|
||||
def start_long_break(duration_min: int | None = None, db: Session = Depends(get_db)) -> dict:
|
||||
try:
|
||||
return PomodoroService(db).start_long_break(duration_min=duration_min)
|
||||
except ValueError as exc:
|
||||
raise _handle_value_error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/cycle/reset")
|
||||
def reset_cycle(clear_task: bool = False, db: Session = Depends(get_db)) -> dict:
|
||||
return PomodoroService(db).reset_cycle(clear_task=clear_task)
|
||||
|
||||
|
||||
@router.post("/skip")
|
||||
def skip_phase(db: Session = Depends(get_db)) -> dict:
|
||||
try:
|
||||
return PomodoroService(db).skip_phase()
|
||||
except ValueError as exc:
|
||||
raise _handle_value_error(exc) from exc
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.schemas import PomodoroStart, PomodoroStop
|
||||
from app.auth.deps import get_current_user
|
||||
from app.db.base import get_db
|
||||
from app.db.models import User
|
||||
from app.pomodoro.service import PomodoroService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _handle_value_error(exc: ValueError) -> HTTPException:
|
||||
return HTTPException(status_code=400, detail=str(exc))
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
def get_status(db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict:
|
||||
return PomodoroService(db, user.id).get_status()
|
||||
|
||||
|
||||
@router.post("/start")
|
||||
def start_pomodoro(payload: PomodoroStart, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict:
|
||||
try:
|
||||
return PomodoroService(db, user.id).start(
|
||||
duration_min=payload.duration_min,
|
||||
task_note=payload.task_note,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise _handle_value_error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/pause")
|
||||
def pause_pomodoro(db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict:
|
||||
try:
|
||||
return PomodoroService(db, user.id).pause()
|
||||
except ValueError as exc:
|
||||
raise _handle_value_error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/resume")
|
||||
def resume_pomodoro(db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict:
|
||||
try:
|
||||
return PomodoroService(db, user.id).resume()
|
||||
except ValueError as exc:
|
||||
raise _handle_value_error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/stop")
|
||||
def stop_pomodoro(payload: PomodoroStop, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict:
|
||||
try:
|
||||
return PomodoroService(db, user.id).stop(
|
||||
result=payload.result,
|
||||
completed=payload.completed,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise _handle_value_error(exc) from exc
|
||||
|
||||
|
||||
@router.get("/history")
|
||||
def get_history(limit: int = 20, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> list[dict]:
|
||||
return PomodoroService(db, user.id).history(limit=limit)
|
||||
|
||||
|
||||
@router.post("/work/start")
|
||||
def start_work(payload: PomodoroStart, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict:
|
||||
try:
|
||||
return PomodoroService(db, user.id).start_work(
|
||||
duration_min=payload.duration_min,
|
||||
task_note=payload.task_note,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise _handle_value_error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/break/short/start")
|
||||
def start_short_break(duration_min: int | None = None, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict:
|
||||
try:
|
||||
return PomodoroService(db, user.id).start_short_break(duration_min=duration_min)
|
||||
except ValueError as exc:
|
||||
raise _handle_value_error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/break/long/start")
|
||||
def start_long_break(duration_min: int | None = None, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict:
|
||||
try:
|
||||
return PomodoroService(db, user.id).start_long_break(duration_min=duration_min)
|
||||
except ValueError as exc:
|
||||
raise _handle_value_error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/cycle/reset")
|
||||
def reset_cycle(clear_task: bool = False, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict:
|
||||
return PomodoroService(db, user.id).reset_cycle(clear_task=clear_task)
|
||||
|
||||
|
||||
@router.post("/skip")
|
||||
def skip_phase(db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict:
|
||||
try:
|
||||
return PomodoroService(db, user.id).skip_phase()
|
||||
except ValueError as exc:
|
||||
raise _handle_value_error(exc) from exc
|
||||
|
||||
@@ -1,76 +1,78 @@
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.base import get_db
|
||||
from app.projects.service import ProjectService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class GiteaBinding(BaseModel):
|
||||
gitea_owner: str = Field(min_length=1)
|
||||
gitea_repo: str = Field(min_length=1)
|
||||
default_branch: str = "main"
|
||||
|
||||
|
||||
class WorkItemCreate(BaseModel):
|
||||
text: str = Field(min_length=1)
|
||||
project_slug: str | None = None
|
||||
|
||||
|
||||
@router.get("/projects")
|
||||
def list_projects(db: Session = Depends(get_db)) -> list[dict[str, Any]]:
|
||||
return ProjectService(db).list_projects()
|
||||
|
||||
|
||||
@router.post("/projects/sync-taiga")
|
||||
def sync_taiga_projects(db: Session = Depends(get_db)) -> list[dict[str, Any]]:
|
||||
try:
|
||||
return ProjectService(db).sync_taiga_projects()
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.put("/projects/{taiga_slug}/gitea")
|
||||
def bind_gitea(
|
||||
taiga_slug: str,
|
||||
payload: GiteaBinding,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return ProjectService(db).bind_gitea(
|
||||
taiga_slug,
|
||||
payload.gitea_owner,
|
||||
payload.gitea_repo,
|
||||
payload.default_branch,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/work-items")
|
||||
async def create_work_item(
|
||||
payload: WorkItemCreate,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await ProjectService(db).create_work_item(
|
||||
payload.text,
|
||||
project_slug=payload.project_slug,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/work-items")
|
||||
def list_work_items(
|
||||
limit: int = 30,
|
||||
status: str | None = None,
|
||||
db: Session = Depends(get_db),
|
||||
) -> list[dict[str, Any]]:
|
||||
return ProjectService(db).list_work_items(limit=limit, status=status)
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.auth.deps import get_current_user
|
||||
from app.db.base import get_db
|
||||
from app.db.models import User
|
||||
from app.projects.service import ProjectService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class GiteaBinding(BaseModel):
|
||||
gitea_owner: str = Field(min_length=1)
|
||||
gitea_repo: str = Field(min_length=1)
|
||||
default_branch: str = "main"
|
||||
|
||||
|
||||
class WorkItemCreate(BaseModel):
|
||||
text: str = Field(min_length=1)
|
||||
project_slug: str | None = None
|
||||
|
||||
|
||||
@router.get("/projects")
|
||||
def list_projects(db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> list[dict[str, Any]]:
|
||||
return ProjectService(db, user.id).list_projects()
|
||||
|
||||
|
||||
@router.post("/projects/sync-taiga")
|
||||
def sync_taiga_projects(db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> list[dict[str, Any]]:
|
||||
try:
|
||||
return ProjectService(db, user.id).sync_taiga_projects()
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.put("/projects/{taiga_slug}/gitea")
|
||||
def bind_gitea(
|
||||
taiga_slug: str,
|
||||
payload: GiteaBinding,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return ProjectService(db, user.id).bind_gitea(
|
||||
taiga_slug,
|
||||
payload.gitea_owner,
|
||||
payload.gitea_repo,
|
||||
payload.default_branch,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/work-items")
|
||||
async def create_work_item(
|
||||
payload: WorkItemCreate,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await ProjectService(db, user.id).create_work_item(
|
||||
payload.text,
|
||||
project_slug=payload.project_slug,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/work-items")
|
||||
def list_work_items(
|
||||
limit: int = 30,
|
||||
status: str | None = None,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> list[dict[str, Any]]:
|
||||
return ProjectService(db, user.id).list_work_items(limit=limit, status=status)
|
||||
|
||||
+128
-124
@@ -1,124 +1,128 @@
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.base import get_db
|
||||
from app.homelab.context import resolve_timezone
|
||||
from app.reminders.service import RemindersService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ReminderCreate(BaseModel):
|
||||
title: str = Field(min_length=1, max_length=255)
|
||||
due_at: str = Field(description="ISO datetime, например 2027-05-12T12:16:00")
|
||||
notes: str = ""
|
||||
all_day: bool = False
|
||||
recurrence: str = "none"
|
||||
|
||||
|
||||
class ReminderUpdate(BaseModel):
|
||||
title: str | None = Field(default=None, min_length=1, max_length=255)
|
||||
due_at: str | None = None
|
||||
notes: str | None = None
|
||||
all_day: bool | None = None
|
||||
recurrence: str | None = None
|
||||
enabled: bool | None = None
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_snapshot(db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
return RemindersService(db).snapshot()
|
||||
|
||||
|
||||
@router.get("/upcoming")
|
||||
def list_upcoming(
|
||||
limit: int = Query(30, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
) -> list[dict[str, Any]]:
|
||||
return RemindersService(db).list_upcoming(limit=limit)
|
||||
|
||||
|
||||
@router.get("/calendar")
|
||||
def calendar(
|
||||
year: int = Query(..., ge=2000, le=2100),
|
||||
month: int = Query(..., ge=1, le=12),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
tz_name = resolve_timezone(db)
|
||||
try:
|
||||
tz = ZoneInfo(tz_name)
|
||||
except Exception:
|
||||
tz = ZoneInfo("Europe/Moscow")
|
||||
|
||||
start = datetime(year, month, 1, tzinfo=tz)
|
||||
if month == 12:
|
||||
end = datetime(year + 1, 1, 1, tzinfo=tz)
|
||||
else:
|
||||
end = datetime(year, month + 1, 1, tzinfo=tz)
|
||||
|
||||
service = RemindersService(db)
|
||||
items = service.list_in_range(
|
||||
date_from=start.astimezone(timezone.utc),
|
||||
date_to=end.astimezone(timezone.utc),
|
||||
)
|
||||
return {
|
||||
"year": year,
|
||||
"month": month,
|
||||
"timezone": tz_name,
|
||||
"reminders": items,
|
||||
}
|
||||
|
||||
|
||||
@router.post("")
|
||||
def create_reminder(payload: ReminderCreate, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
try:
|
||||
return RemindersService(db).create(
|
||||
title=payload.title,
|
||||
due_at=payload.due_at,
|
||||
notes=payload.notes,
|
||||
all_day=payload.all_day,
|
||||
recurrence=payload.recurrence,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.patch("/{reminder_id}")
|
||||
def update_reminder(
|
||||
reminder_id: int,
|
||||
payload: ReminderUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return RemindersService(db).update(
|
||||
reminder_id,
|
||||
title=payload.title,
|
||||
due_at=payload.due_at,
|
||||
notes=payload.notes,
|
||||
all_day=payload.all_day,
|
||||
recurrence=payload.recurrence,
|
||||
enabled=payload.enabled,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.delete("/{reminder_id}")
|
||||
def delete_reminder(reminder_id: int, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
try:
|
||||
return RemindersService(db).delete(reminder_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/{reminder_id}/complete")
|
||||
def complete_reminder(reminder_id: int, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
try:
|
||||
return RemindersService(db).complete(reminder_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.auth.deps import get_current_user
|
||||
from app.db.base import get_db
|
||||
from app.db.models import User
|
||||
from app.homelab.context import resolve_timezone
|
||||
from app.reminders_scoped.service import RemindersService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ReminderCreate(BaseModel):
|
||||
title: str = Field(min_length=1, max_length=255)
|
||||
due_at: str = Field(description="ISO datetime, например 2027-05-12T12:16:00")
|
||||
notes: str = ""
|
||||
all_day: bool = False
|
||||
recurrence: str = "none"
|
||||
|
||||
|
||||
class ReminderUpdate(BaseModel):
|
||||
title: str | None = Field(default=None, min_length=1, max_length=255)
|
||||
due_at: str | None = None
|
||||
notes: str | None = None
|
||||
all_day: bool | None = None
|
||||
recurrence: str | None = None
|
||||
enabled: bool | None = None
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_snapshot(db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, Any]:
|
||||
return RemindersService(db, user.id).snapshot()
|
||||
|
||||
|
||||
@router.get("/upcoming")
|
||||
def list_upcoming(
|
||||
limit: int = Query(30, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
) -> list[dict[str, Any]]:
|
||||
return RemindersService(db, user.id).list_upcoming(limit=limit)
|
||||
|
||||
|
||||
@router.get("/calendar")
|
||||
def calendar(
|
||||
year: int = Query(..., ge=2000, le=2100),
|
||||
month: int = Query(..., ge=1, le=12),
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
tz_name = resolve_timezone(db, user.id)
|
||||
try:
|
||||
tz = ZoneInfo(tz_name)
|
||||
except Exception:
|
||||
tz = ZoneInfo("Europe/Moscow")
|
||||
|
||||
start = datetime(year, month, 1, tzinfo=tz)
|
||||
if month == 12:
|
||||
end = datetime(year + 1, 1, 1, tzinfo=tz)
|
||||
else:
|
||||
end = datetime(year, month + 1, 1, tzinfo=tz)
|
||||
|
||||
service = RemindersService(db, user.id)
|
||||
items = service.list_in_range(
|
||||
date_from=start.astimezone(timezone.utc),
|
||||
date_to=end.astimezone(timezone.utc),
|
||||
)
|
||||
return {
|
||||
"year": year,
|
||||
"month": month,
|
||||
"timezone": tz_name,
|
||||
"reminders": items,
|
||||
}
|
||||
|
||||
|
||||
@router.post("")
|
||||
def create_reminder(payload: ReminderCreate, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, Any]:
|
||||
try:
|
||||
return RemindersService(db, user.id).create(
|
||||
title=payload.title,
|
||||
due_at=payload.due_at,
|
||||
notes=payload.notes,
|
||||
all_day=payload.all_day,
|
||||
recurrence=payload.recurrence,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.patch("/{reminder_id}")
|
||||
def update_reminder(
|
||||
reminder_id: int,
|
||||
payload: ReminderUpdate,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return RemindersService(db, user.id).update(
|
||||
reminder_id,
|
||||
title=payload.title,
|
||||
due_at=payload.due_at,
|
||||
notes=payload.notes,
|
||||
all_day=payload.all_day,
|
||||
recurrence=payload.recurrence,
|
||||
enabled=payload.enabled,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.delete("/{reminder_id}")
|
||||
def delete_reminder(reminder_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, Any]:
|
||||
try:
|
||||
return RemindersService(db, user.id).delete(reminder_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/{reminder_id}/complete")
|
||||
def complete_reminder(reminder_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, Any]:
|
||||
try:
|
||||
return RemindersService(db, user.id).complete(reminder_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.auth.deps import get_current_user
|
||||
from app.db.base import get_db
|
||||
from app.db.models import User
|
||||
from app.settings.service import SETTING_KEYS, SettingsService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class SettingsPatch(BaseModel):
|
||||
openrouter_model: str | None = None
|
||||
memory_extract_model: str | None = None
|
||||
openrouter_reasoning_effort: str | None = None
|
||||
rag_enabled: bool | None = None
|
||||
rag_top_k: int | None = Field(default=None, ge=1, le=50)
|
||||
|
||||
|
||||
@router.get("/settings")
|
||||
def get_settings_route(db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, Any]:
|
||||
return SettingsService(db).snapshot()
|
||||
|
||||
|
||||
@router.patch("/settings")
|
||||
def patch_settings_route(
|
||||
payload: SettingsPatch,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
updates = payload.model_dump(exclude_unset=True)
|
||||
return SettingsService(db).patch(updates)
|
||||
+118
-116
@@ -1,116 +1,118 @@
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.base import get_db
|
||||
from app.shopping.service import ShoppingService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ListCreate(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=255)
|
||||
|
||||
|
||||
class ListRename(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=255)
|
||||
|
||||
|
||||
class ItemInput(BaseModel):
|
||||
text: str = Field(min_length=1, max_length=500)
|
||||
quantity: float | None = None
|
||||
unit: str = ""
|
||||
|
||||
|
||||
class ItemsAdd(BaseModel):
|
||||
list_id: int | None = None
|
||||
list_name: str | None = None
|
||||
items: list[ItemInput] = Field(min_length=1)
|
||||
|
||||
|
||||
class ItemChecked(BaseModel):
|
||||
checked: bool
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_snapshot(db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
return ShoppingService(db).snapshot()
|
||||
|
||||
|
||||
@router.get("/lists")
|
||||
def list_lists(db: Session = Depends(get_db)) -> list[dict[str, Any]]:
|
||||
return ShoppingService(db).list_lists(include_items=True)
|
||||
|
||||
|
||||
@router.post("/lists")
|
||||
def create_list(payload: ListCreate, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
try:
|
||||
return ShoppingService(db).create_list(payload.name)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/lists/{list_id}")
|
||||
def get_list(list_id: int, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
data = ShoppingService(db).get_list(list_id=list_id)
|
||||
if not data:
|
||||
raise HTTPException(status_code=404, detail="List not found")
|
||||
return data
|
||||
|
||||
|
||||
@router.patch("/lists/{list_id}")
|
||||
def rename_list(list_id: int, payload: ListRename, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
try:
|
||||
return ShoppingService(db).rename_list(list_id, payload.name)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.delete("/lists/{list_id}")
|
||||
def delete_list(list_id: int, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
try:
|
||||
return ShoppingService(db).delete_list(list_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/items")
|
||||
def add_items(payload: ItemsAdd, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
try:
|
||||
return ShoppingService(db).add_items(
|
||||
[i.model_dump() for i in payload.items],
|
||||
list_id=payload.list_id,
|
||||
list_name=payload.list_name,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.patch("/items/{item_id}")
|
||||
def set_item_checked(
|
||||
item_id: int,
|
||||
payload: ItemChecked,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return ShoppingService(db).set_item_checked(item_id, payload.checked)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.delete("/items/{item_id}")
|
||||
def remove_item(item_id: int, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
try:
|
||||
return ShoppingService(db).remove_item(item_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/lists/{list_id}/clear-checked")
|
||||
def clear_checked(list_id: int, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
try:
|
||||
return ShoppingService(db).clear_checked(list_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.auth.deps import get_current_user
|
||||
from app.db.base import get_db
|
||||
from app.db.models import User
|
||||
from app.shopping.service import ShoppingService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ListCreate(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=255)
|
||||
|
||||
|
||||
class ListRename(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=255)
|
||||
|
||||
|
||||
class ItemInput(BaseModel):
|
||||
text: str = Field(min_length=1, max_length=500)
|
||||
quantity: float | None = None
|
||||
unit: str = ""
|
||||
|
||||
|
||||
class ItemsAdd(BaseModel):
|
||||
list_id: int | None = None
|
||||
list_name: str | None = None
|
||||
items: list[ItemInput] = Field(min_length=1)
|
||||
|
||||
|
||||
class ItemChecked(BaseModel):
|
||||
checked: bool
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_snapshot(db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, Any]:
|
||||
return ShoppingService(db, user.id).snapshot()
|
||||
|
||||
|
||||
@router.get("/lists")
|
||||
def list_lists(db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> list[dict[str, Any]]:
|
||||
return ShoppingService(db, user.id).list_lists(include_items=True)
|
||||
|
||||
|
||||
@router.post("/lists")
|
||||
def create_list(payload: ListCreate, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, Any]:
|
||||
try:
|
||||
return ShoppingService(db, user.id).create_list(payload.name)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/lists/{list_id}")
|
||||
def get_list(list_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, Any]:
|
||||
data = ShoppingService(db, user.id).get_list(list_id=list_id)
|
||||
if not data:
|
||||
raise HTTPException(status_code=404, detail="List not found")
|
||||
return data
|
||||
|
||||
|
||||
@router.patch("/lists/{list_id}")
|
||||
def rename_list(list_id: int, payload: ListRename, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, Any]:
|
||||
try:
|
||||
return ShoppingService(db, user.id).rename_list(list_id, payload.name)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.delete("/lists/{list_id}")
|
||||
def delete_list(list_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, Any]:
|
||||
try:
|
||||
return ShoppingService(db, user.id).delete_list(list_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/items")
|
||||
def add_items(payload: ItemsAdd, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, Any]:
|
||||
try:
|
||||
return ShoppingService(db, user.id).add_items(
|
||||
[i.model_dump() for i in payload.items],
|
||||
list_id=payload.list_id,
|
||||
list_name=payload.list_name,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.patch("/items/{item_id}")
|
||||
def set_item_checked(
|
||||
item_id: int,
|
||||
payload: ItemChecked,
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return ShoppingService(db, user.id).set_item_checked(item_id, payload.checked)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.delete("/items/{item_id}")
|
||||
def remove_item(item_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, Any]:
|
||||
try:
|
||||
return ShoppingService(db, user.id).remove_item(item_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/lists/{list_id}/clear-checked")
|
||||
def clear_checked(list_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, Any]:
|
||||
try:
|
||||
return ShoppingService(db, user.id).clear_checked(list_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
+106
-118
@@ -1,118 +1,106 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.config import get_settings
|
||||
from app.db.base import SessionLocal, get_db
|
||||
from app.db.models import ChatSession, Message, ProjectBinding
|
||||
from app.projects.service import ProjectService
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _verify_gitea_signature(body: bytes, signature: str | None, secret: str) -> bool:
|
||||
if not secret:
|
||||
return True
|
||||
if not signature:
|
||||
return False
|
||||
if signature.startswith("sha256="):
|
||||
signature = signature[7:]
|
||||
expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
|
||||
return hmac.compare_digest(expected, signature)
|
||||
|
||||
|
||||
def _post_close_notice(results: list[dict[str, Any]], owner: str, repo: str) -> None:
|
||||
if not results:
|
||||
return
|
||||
db = SessionLocal()
|
||||
try:
|
||||
session = db.scalar(
|
||||
select(ChatSession).order_by(ChatSession.updated_at.desc()).limit(1)
|
||||
)
|
||||
if not session:
|
||||
session = ChatSession(title="Git")
|
||||
db.add(session)
|
||||
db.commit()
|
||||
db.refresh(session)
|
||||
|
||||
lines = [f"🔀 **Push** `{owner}/{repo}`"]
|
||||
for item in results:
|
||||
if "closed" in item:
|
||||
lines.append(f"- `{item.get('commit', '?')}`: закрыто {item['closed']}")
|
||||
elif "error" in item:
|
||||
lines.append(f"- ошибка: {item['error']}")
|
||||
|
||||
db.add(Message(session_id=session.id, role="notice", content="\n".join(lines)))
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/webhooks/gitea")
|
||||
async def gitea_webhook(request: Request, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
body = await request.body()
|
||||
settings = get_settings()
|
||||
signature = (
|
||||
request.headers.get("X-Gitea-Signature")
|
||||
or request.headers.get("X-Gogs-Signature")
|
||||
or request.headers.get("X-Hub-Signature-256")
|
||||
)
|
||||
|
||||
if not _verify_gitea_signature(body, signature, settings.gitea_webhook_secret):
|
||||
raise HTTPException(status_code=401, detail="Invalid webhook signature")
|
||||
|
||||
payload = json.loads(body)
|
||||
if payload.get("secret") and settings.gitea_webhook_secret:
|
||||
if payload.get("secret") != settings.gitea_webhook_secret:
|
||||
raise HTTPException(status_code=401, detail="Invalid webhook secret")
|
||||
|
||||
event = request.headers.get("X-Gitea-Event", "")
|
||||
if event != "push":
|
||||
return {"ok": True, "skipped": event}
|
||||
|
||||
repo = payload.get("repository", {})
|
||||
owner = repo.get("owner", {}).get("login", "")
|
||||
repo_name = repo.get("name", "")
|
||||
if not owner or not repo_name:
|
||||
raise HTTPException(status_code=400, detail="Missing repository info")
|
||||
|
||||
binding = db.scalar(
|
||||
select(ProjectBinding).where(
|
||||
ProjectBinding.gitea_owner == owner,
|
||||
ProjectBinding.gitea_repo == repo_name,
|
||||
)
|
||||
)
|
||||
if not binding:
|
||||
return {"ok": True, "skipped": "unknown repo"}
|
||||
|
||||
commits = list(payload.get("commits") or [])
|
||||
if not commits:
|
||||
head = payload.get("head_commit")
|
||||
if head:
|
||||
commits = [head]
|
||||
|
||||
logger.info(
|
||||
"Gitea push %s/%s ref=%s commits=%d",
|
||||
owner,
|
||||
repo_name,
|
||||
payload.get("ref", ""),
|
||||
len(commits),
|
||||
)
|
||||
|
||||
service = ProjectService(db)
|
||||
results = service.process_push(owner, repo_name, commits)
|
||||
if results:
|
||||
logger.info("Gitea push results: %s", results)
|
||||
else:
|
||||
logger.warning("Gitea push: no close actions for %s/%s", owner, repo_name)
|
||||
|
||||
_post_close_notice(results, owner, repo_name)
|
||||
|
||||
return {"ok": True, "results": results, "commits_processed": len(commits)}
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.chat.notice_inbox import post_notice_to_latest_chat
|
||||
from app.config import get_settings
|
||||
from app.db.base import get_db
|
||||
from app.db.models import ProjectBinding
|
||||
from app.projects.service import ProjectService
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _verify_gitea_signature(body: bytes, signature: str | None, secret: str) -> bool:
|
||||
if not secret:
|
||||
return True
|
||||
if not signature:
|
||||
return False
|
||||
if signature.startswith("sha256="):
|
||||
signature = signature[7:]
|
||||
expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
|
||||
return hmac.compare_digest(expected, signature)
|
||||
|
||||
|
||||
def _post_close_notice(
|
||||
results: list[dict[str, Any]], owner: str, repo: str, user_id: int
|
||||
) -> None:
|
||||
if not results:
|
||||
return
|
||||
lines = [f"🔀 **Push** `{owner}/{repo}`"]
|
||||
for item in results:
|
||||
if "closed" in item:
|
||||
lines.append(f"- `{item.get('commit', '?')}`: закрыто {item['closed']}")
|
||||
elif "error" in item:
|
||||
lines.append(f"- ошибка: {item['error']}")
|
||||
post_notice_to_latest_chat("\n".join(lines), user_id)
|
||||
|
||||
|
||||
@router.post("/webhooks/gitea")
|
||||
async def gitea_webhook(request: Request, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||
body = await request.body()
|
||||
settings = get_settings()
|
||||
signature = (
|
||||
request.headers.get("X-Gitea-Signature")
|
||||
or request.headers.get("X-Gogs-Signature")
|
||||
or request.headers.get("X-Hub-Signature-256")
|
||||
)
|
||||
|
||||
if not _verify_gitea_signature(body, signature, settings.gitea_webhook_secret):
|
||||
raise HTTPException(status_code=401, detail="Invalid webhook signature")
|
||||
|
||||
payload = json.loads(body)
|
||||
if payload.get("secret") and settings.gitea_webhook_secret:
|
||||
if payload.get("secret") != settings.gitea_webhook_secret:
|
||||
raise HTTPException(status_code=401, detail="Invalid webhook secret")
|
||||
|
||||
event = request.headers.get("X-Gitea-Event", "")
|
||||
if event != "push":
|
||||
return {"ok": True, "skipped": event}
|
||||
|
||||
repo = payload.get("repository", {})
|
||||
owner = repo.get("owner", {}).get("login", "")
|
||||
repo_name = repo.get("name", "")
|
||||
if not owner or not repo_name:
|
||||
raise HTTPException(status_code=400, detail="Missing repository info")
|
||||
|
||||
binding = db.scalar(
|
||||
select(ProjectBinding).where(
|
||||
ProjectBinding.gitea_owner == owner,
|
||||
ProjectBinding.gitea_repo == repo_name,
|
||||
)
|
||||
)
|
||||
if not binding:
|
||||
return {"ok": True, "skipped": "unknown repo"}
|
||||
|
||||
commits = list(payload.get("commits") or [])
|
||||
if not commits:
|
||||
head = payload.get("head_commit")
|
||||
if head:
|
||||
commits = [head]
|
||||
|
||||
logger.info(
|
||||
"Gitea push %s/%s ref=%s commits=%d",
|
||||
owner,
|
||||
repo_name,
|
||||
payload.get("ref", ""),
|
||||
len(commits),
|
||||
)
|
||||
|
||||
service = ProjectService(db, binding.user_id)
|
||||
results = service.process_push(owner, repo_name, commits)
|
||||
if results:
|
||||
logger.info("Gitea push results: %s", results)
|
||||
else:
|
||||
logger.warning("Gitea push: no close actions for %s/%s", owner, repo_name)
|
||||
|
||||
_post_close_notice(results, owner, repo_name, binding.user_id)
|
||||
|
||||
return {"ok": True, "results": results, "commits_processed": len(commits)}
|
||||
|
||||
Reference in New Issue
Block a user