refactor(db): migrate database operations to async/await
Browse files- Replace SQLModel sync engine/session with SQLAlchemy async equivalents
- Update all CRUD operations and API endpoints to use async database calls
- Add asyncpg and python-dateutil dependencies
- Modify lifespan event to await database initialization
- api/routers/chats.py +20 -20
- db/crud/chat.py +21 -29
- db/crud/message.py +10 -9
- db/session.py +18 -10
- main.py +1 -1
- pyproject.toml +2 -0
- uv.lock +36 -0
api/routers/chats.py
CHANGED
|
@@ -2,7 +2,7 @@ import uuid
|
|
| 2 |
from typing import List
|
| 3 |
|
| 4 |
from fastapi import APIRouter, Depends, HTTPException, status
|
| 5 |
-
from
|
| 6 |
|
| 7 |
# --- LangChain Imports ---
|
| 8 |
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, ToolMessage
|
|
@@ -21,11 +21,11 @@ from workflow.title_generator import generate_chat_title
|
|
| 21 |
router = APIRouter()
|
| 22 |
|
| 23 |
# --- Helper Functions ---
|
| 24 |
-
def get_chat_for_user(chat_id: uuid.UUID, user_id: uuid.UUID, db:
|
| 25 |
"""
|
| 26 |
A helper dependency to get a chat and verify the current user owns it.
|
| 27 |
"""
|
| 28 |
-
chat = chat_crud.get_chat_by_id(db, chat_id)
|
| 29 |
if not chat:
|
| 30 |
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Chat not found")
|
| 31 |
if chat.user_id != user_id:
|
|
@@ -56,19 +56,19 @@ def _convert_db_messages_to_langchain(db_messages: List[DBMessage]) -> List[Base
|
|
| 56 |
# --- Chat CRUD Routes ---
|
| 57 |
|
| 58 |
@router.post("/", response_model=ChatReadSimple, status_code=status.HTTP_201_CREATED)
|
| 59 |
-
def create_new_chat(*, db:
|
| 60 |
"""Creates a new, empty chat for the authenticated user."""
|
| 61 |
-
return chat_crud.create_chat(db=db, user_id=user_id)
|
| 62 |
|
| 63 |
@router.get("/", response_model=List[ChatReadSimple])
|
| 64 |
-
def get_user_chats(*, db:
|
| 65 |
"""Retrieves all chats for the authenticated user."""
|
| 66 |
-
return chat_crud.get_chats_by_user(db=db, user_id=user_id)
|
| 67 |
|
| 68 |
@router.get("/{chat_id}", response_model=ChatReadWithMessages)
|
| 69 |
-
def get_single_chat_with_messages(*, chat_id: uuid.UUID, user_id: uuid.UUID = Depends(get_current_user), db:
|
| 70 |
"""Retrieves a specific chat with all its messages."""
|
| 71 |
-
chat = get_chat_for_user(chat_id, user_id, db)
|
| 72 |
filtered_messages = [
|
| 73 |
msg for msg in chat.messages
|
| 74 |
if msg.role == 'user' or (msg.role == 'assistant' and msg.content)
|
|
@@ -83,16 +83,16 @@ def get_single_chat_with_messages(*, chat_id: uuid.UUID, user_id: uuid.UUID = De
|
|
| 83 |
)
|
| 84 |
|
| 85 |
@router.patch("/{chat_id}", response_model=ChatReadSimple)
|
| 86 |
-
def rename_chat(*, chat_id: uuid.UUID, chat_update: ChatUpdate, user_id: uuid.UUID = Depends(get_current_user), db:
|
| 87 |
"""Renames a specific chat."""
|
| 88 |
-
chat = get_chat_for_user(chat_id, user_id, db)
|
| 89 |
-
return chat_crud.update_chat_title(db=db, chat=chat, chat_update=chat_update)
|
| 90 |
|
| 91 |
@router.delete("/{chat_id}", status_code=status.HTTP_204_NO_CONTENT)
|
| 92 |
-
def remove_chat(*, chat_id: uuid.UUID, user_id: uuid.UUID = Depends(get_current_user), db:
|
| 93 |
"""Deletes a specific chat and all its messages."""
|
| 94 |
-
chat = get_chat_for_user(chat_id, user_id, db)
|
| 95 |
-
chat_crud.delete_chat(db=db, chat=chat)
|
| 96 |
return
|
| 97 |
|
| 98 |
|
|
@@ -104,16 +104,16 @@ async def post_message_and_get_response(
|
|
| 104 |
chat_id: uuid.UUID,
|
| 105 |
message_in: MessageCreate,
|
| 106 |
user_id: uuid.UUID = Depends(get_current_user),
|
| 107 |
-
db:
|
| 108 |
):
|
| 109 |
"""
|
| 110 |
Handles a user's message by invoking the agent and persisting the full turn.
|
| 111 |
"""
|
| 112 |
# 1. Get and verify chat ownership
|
| 113 |
-
chat = get_chat_for_user(chat_id, user_id, db)
|
| 114 |
|
| 115 |
# 2. Load and convert history for the agent
|
| 116 |
-
db_messages = message_crud.get_messages_by_chat(db, chat_id)
|
| 117 |
is_first_user_message = not db_messages
|
| 118 |
messages_for_agent: list[BaseMessage] = _convert_db_messages_to_langchain(db_messages)
|
| 119 |
|
|
@@ -132,7 +132,7 @@ async def post_message_and_get_response(
|
|
| 132 |
new_lc_messages = updated_messages_from_agent[initial_message_count - 1:]
|
| 133 |
|
| 134 |
# 7. Atomically persist the entire turn to the database
|
| 135 |
-
final_ai_message = message_crud.create_messages_for_turn(
|
| 136 |
db=db,
|
| 137 |
chat_id=chat_id,
|
| 138 |
new_lc_messages=new_lc_messages,
|
|
@@ -143,6 +143,6 @@ async def post_message_and_get_response(
|
|
| 143 |
# 8. If this was the first message, generate and set a title for the chat
|
| 144 |
if is_first_user_message:
|
| 145 |
new_title = await generate_chat_title(message_in.content)
|
| 146 |
-
chat_crud.update_chat_title(db=db, chat=chat, chat_update=ChatUpdate(title=new_title))
|
| 147 |
|
| 148 |
return final_ai_message
|
|
|
|
| 2 |
from typing import List
|
| 3 |
|
| 4 |
from fastapi import APIRouter, Depends, HTTPException, status
|
| 5 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
| 6 |
|
| 7 |
# --- LangChain Imports ---
|
| 8 |
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, ToolMessage
|
|
|
|
| 21 |
router = APIRouter()
|
| 22 |
|
| 23 |
# --- Helper Functions ---
|
| 24 |
+
async def get_chat_for_user(chat_id: uuid.UUID, user_id: uuid.UUID, db: AsyncSession) -> Chat:
|
| 25 |
"""
|
| 26 |
A helper dependency to get a chat and verify the current user owns it.
|
| 27 |
"""
|
| 28 |
+
chat = await chat_crud.get_chat_by_id(db, chat_id)
|
| 29 |
if not chat:
|
| 30 |
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Chat not found")
|
| 31 |
if chat.user_id != user_id:
|
|
|
|
| 56 |
# --- Chat CRUD Routes ---
|
| 57 |
|
| 58 |
@router.post("/", response_model=ChatReadSimple, status_code=status.HTTP_201_CREATED)
|
| 59 |
+
async def create_new_chat(*, db: AsyncSession = Depends(get_db), user_id: uuid.UUID = Depends(get_current_user)):
|
| 60 |
"""Creates a new, empty chat for the authenticated user."""
|
| 61 |
+
return await chat_crud.create_chat(db=db, user_id=user_id)
|
| 62 |
|
| 63 |
@router.get("/", response_model=List[ChatReadSimple])
|
| 64 |
+
async def get_user_chats(*, db: AsyncSession = Depends(get_db), user_id: uuid.UUID = Depends(get_current_user)):
|
| 65 |
"""Retrieves all chats for the authenticated user."""
|
| 66 |
+
return await chat_crud.get_chats_by_user(db=db, user_id=user_id)
|
| 67 |
|
| 68 |
@router.get("/{chat_id}", response_model=ChatReadWithMessages)
|
| 69 |
+
async def get_single_chat_with_messages(*, chat_id: uuid.UUID, user_id: uuid.UUID = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
| 70 |
"""Retrieves a specific chat with all its messages."""
|
| 71 |
+
chat = await get_chat_for_user(chat_id, user_id, db)
|
| 72 |
filtered_messages = [
|
| 73 |
msg for msg in chat.messages
|
| 74 |
if msg.role == 'user' or (msg.role == 'assistant' and msg.content)
|
|
|
|
| 83 |
)
|
| 84 |
|
| 85 |
@router.patch("/{chat_id}", response_model=ChatReadSimple)
|
| 86 |
+
async def rename_chat(*, chat_id: uuid.UUID, chat_update: ChatUpdate, user_id: uuid.UUID = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
| 87 |
"""Renames a specific chat."""
|
| 88 |
+
chat = await get_chat_for_user(chat_id, user_id, db)
|
| 89 |
+
return await chat_crud.update_chat_title(db=db, chat=chat, chat_update=chat_update)
|
| 90 |
|
| 91 |
@router.delete("/{chat_id}", status_code=status.HTTP_204_NO_CONTENT)
|
| 92 |
+
async def remove_chat(*, chat_id: uuid.UUID, user_id: uuid.UUID = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
| 93 |
"""Deletes a specific chat and all its messages."""
|
| 94 |
+
chat = await get_chat_for_user(chat_id, user_id, db)
|
| 95 |
+
await chat_crud.delete_chat(db=db, chat=chat)
|
| 96 |
return
|
| 97 |
|
| 98 |
|
|
|
|
| 104 |
chat_id: uuid.UUID,
|
| 105 |
message_in: MessageCreate,
|
| 106 |
user_id: uuid.UUID = Depends(get_current_user),
|
| 107 |
+
db: AsyncSession = Depends(get_db)
|
| 108 |
):
|
| 109 |
"""
|
| 110 |
Handles a user's message by invoking the agent and persisting the full turn.
|
| 111 |
"""
|
| 112 |
# 1. Get and verify chat ownership
|
| 113 |
+
chat = await get_chat_for_user(chat_id, user_id, db)
|
| 114 |
|
| 115 |
# 2. Load and convert history for the agent
|
| 116 |
+
db_messages = await message_crud.get_messages_by_chat(db, chat_id)
|
| 117 |
is_first_user_message = not db_messages
|
| 118 |
messages_for_agent: list[BaseMessage] = _convert_db_messages_to_langchain(db_messages)
|
| 119 |
|
|
|
|
| 132 |
new_lc_messages = updated_messages_from_agent[initial_message_count - 1:]
|
| 133 |
|
| 134 |
# 7. Atomically persist the entire turn to the database
|
| 135 |
+
final_ai_message = await message_crud.create_messages_for_turn(
|
| 136 |
db=db,
|
| 137 |
chat_id=chat_id,
|
| 138 |
new_lc_messages=new_lc_messages,
|
|
|
|
| 143 |
# 8. If this was the first message, generate and set a title for the chat
|
| 144 |
if is_first_user_message:
|
| 145 |
new_title = await generate_chat_title(message_in.content)
|
| 146 |
+
await chat_crud.update_chat_title(db=db, chat=chat, chat_update=ChatUpdate(title=new_title))
|
| 147 |
|
| 148 |
return final_ai_message
|
db/crud/chat.py
CHANGED
|
@@ -1,49 +1,41 @@
|
|
| 1 |
import uuid
|
| 2 |
from datetime import datetime, timezone
|
| 3 |
from typing import Optional
|
|
|
|
|
|
|
| 4 |
from sqlmodel import Session, select
|
| 5 |
|
| 6 |
from db.models.chat import Chat
|
| 7 |
from db.schemas.chat import ChatUpdate
|
| 8 |
|
| 9 |
-
def create_chat(db:
|
| 10 |
-
"""
|
| 11 |
-
Creates a new, empty chat for a specific user.
|
| 12 |
-
"""
|
| 13 |
db_chat = Chat(user_id=user_id)
|
| 14 |
db.add(db_chat)
|
| 15 |
-
db.commit()
|
| 16 |
-
db.refresh(db_chat)
|
| 17 |
return db_chat
|
| 18 |
|
| 19 |
-
def get_chats_by_user(db:
|
| 20 |
-
"""
|
| 21 |
-
Retrieves all chats for a specific user, sorted by the most recently updated.
|
| 22 |
-
"""
|
| 23 |
statement = select(Chat).where(Chat.user_id == user_id).order_by(Chat.updated_at.desc())
|
| 24 |
-
|
|
|
|
| 25 |
|
| 26 |
-
def get_chat_by_id(db:
|
| 27 |
-
"""
|
| 28 |
-
|
| 29 |
-
Returns None if the chat does not exist.
|
| 30 |
-
"""
|
| 31 |
-
return db.get(Chat, chat_id)
|
| 32 |
|
| 33 |
-
def update_chat_title(db:
|
| 34 |
-
"""
|
| 35 |
-
Updates a chat's title and its updated_at timestamp.
|
| 36 |
-
"""
|
| 37 |
chat.title = chat_update.title
|
| 38 |
chat.updated_at = datetime.now(timezone.utc)
|
| 39 |
db.add(chat)
|
| 40 |
-
db.commit()
|
| 41 |
-
db.refresh(chat)
|
| 42 |
return chat
|
| 43 |
|
| 44 |
-
def delete_chat(db:
|
| 45 |
-
"""
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
db.delete(chat)
|
| 49 |
-
db.commit()
|
|
|
|
| 1 |
import uuid
|
| 2 |
from datetime import datetime, timezone
|
| 3 |
from typing import Optional
|
| 4 |
+
|
| 5 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
| 6 |
from sqlmodel import Session, select
|
| 7 |
|
| 8 |
from db.models.chat import Chat
|
| 9 |
from db.schemas.chat import ChatUpdate
|
| 10 |
|
| 11 |
+
async def create_chat(db: AsyncSession, user_id: uuid.UUID) -> Chat:
|
| 12 |
+
"""Creates a new, empty chat for a specific user asynchronously."""
|
|
|
|
|
|
|
| 13 |
db_chat = Chat(user_id=user_id)
|
| 14 |
db.add(db_chat)
|
| 15 |
+
await db.commit()
|
| 16 |
+
await db.refresh(db_chat)
|
| 17 |
return db_chat
|
| 18 |
|
| 19 |
+
async def get_chats_by_user(db: AsyncSession, user_id: uuid.UUID) -> list[Chat]:
|
| 20 |
+
"""Retrieves all chats for a specific user, sorted by the most recently updated."""
|
|
|
|
|
|
|
| 21 |
statement = select(Chat).where(Chat.user_id == user_id).order_by(Chat.updated_at.desc())
|
| 22 |
+
result = await db.exec(statement)
|
| 23 |
+
return result.all()
|
| 24 |
|
| 25 |
+
async def get_chat_by_id(db: AsyncSession, chat_id: uuid.UUID) -> Optional[Chat]:
|
| 26 |
+
"""Retrieves a single chat by its unique ID."""
|
| 27 |
+
return await db.get(Chat, chat_id)
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
+
async def update_chat_title(db: AsyncSession, chat: Chat, chat_update: ChatUpdate) -> Chat:
|
| 30 |
+
"""Updates a chat's title and its updated_at timestamp."""
|
|
|
|
|
|
|
| 31 |
chat.title = chat_update.title
|
| 32 |
chat.updated_at = datetime.now(timezone.utc)
|
| 33 |
db.add(chat)
|
| 34 |
+
await db.commit()
|
| 35 |
+
await db.refresh(chat)
|
| 36 |
return chat
|
| 37 |
|
| 38 |
+
async def delete_chat(db: AsyncSession, chat: Chat) -> None:
|
| 39 |
+
"""Deletes a chat from the database."""
|
| 40 |
+
await db.delete(chat)
|
| 41 |
+
await db.commit()
|
|
|
|
|
|
db/crud/message.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import uuid
|
| 2 |
-
from sqlmodel import
|
|
|
|
| 3 |
from datetime import datetime, timezone
|
| 4 |
|
| 5 |
from db.models.chat import Chat
|
|
@@ -7,8 +8,8 @@ from db.models.message import Message
|
|
| 7 |
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, ToolMessage
|
| 8 |
|
| 9 |
|
| 10 |
-
def create_messages_for_turn(
|
| 11 |
-
db:
|
| 12 |
chat_id: uuid.UUID,
|
| 13 |
new_lc_messages: list[BaseMessage],
|
| 14 |
final_answer: str,
|
|
@@ -34,7 +35,7 @@ def create_messages_for_turn(
|
|
| 34 |
"""
|
| 35 |
# First, get the parent chat to update its timestamp.
|
| 36 |
# This also ensures the chat exists before we try to add messages to it.
|
| 37 |
-
chat = db.get(Chat, chat_id)
|
| 38 |
if not chat:
|
| 39 |
# Or raise a custom exception that can be caught by the API layer
|
| 40 |
raise ValueError("Chat not found")
|
|
@@ -73,17 +74,17 @@ def create_messages_for_turn(
|
|
| 73 |
db_messages_to_add.append(db_msg)
|
| 74 |
|
| 75 |
# Use add_all for efficient bulk insertion
|
| 76 |
-
db.add_all(db_messages_to_add)
|
| 77 |
-
db.commit()
|
| 78 |
|
| 79 |
# Only refresh the single message we need to return to the client.
|
| 80 |
final_ai_message = db_messages_to_add[-1]
|
| 81 |
-
db.refresh(final_ai_message)
|
| 82 |
|
| 83 |
return final_ai_message
|
| 84 |
|
| 85 |
|
| 86 |
-
def get_messages_by_chat(db:
|
| 87 |
"""
|
| 88 |
Retrieves all messages for a specific chat, sorted by creation time
|
| 89 |
to ensure correct conversation order.
|
|
@@ -93,4 +94,4 @@ def get_messages_by_chat(db: Session, chat_id: uuid.UUID) -> list[Message]:
|
|
| 93 |
.where(Message.chat_id == chat_id)
|
| 94 |
.order_by(Message.created_at.asc())
|
| 95 |
)
|
| 96 |
-
return db.exec(statement).all()
|
|
|
|
| 1 |
import uuid
|
| 2 |
+
from sqlmodel import select
|
| 3 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
| 4 |
from datetime import datetime, timezone
|
| 5 |
|
| 6 |
from db.models.chat import Chat
|
|
|
|
| 8 |
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, ToolMessage
|
| 9 |
|
| 10 |
|
| 11 |
+
async def create_messages_for_turn(
|
| 12 |
+
db: AsyncSession,
|
| 13 |
chat_id: uuid.UUID,
|
| 14 |
new_lc_messages: list[BaseMessage],
|
| 15 |
final_answer: str,
|
|
|
|
| 35 |
"""
|
| 36 |
# First, get the parent chat to update its timestamp.
|
| 37 |
# This also ensures the chat exists before we try to add messages to it.
|
| 38 |
+
chat = await db.get(Chat, chat_id)
|
| 39 |
if not chat:
|
| 40 |
# Or raise a custom exception that can be caught by the API layer
|
| 41 |
raise ValueError("Chat not found")
|
|
|
|
| 74 |
db_messages_to_add.append(db_msg)
|
| 75 |
|
| 76 |
# Use add_all for efficient bulk insertion
|
| 77 |
+
await db.add_all(db_messages_to_add)
|
| 78 |
+
await db.commit()
|
| 79 |
|
| 80 |
# Only refresh the single message we need to return to the client.
|
| 81 |
final_ai_message = db_messages_to_add[-1]
|
| 82 |
+
await db.refresh(final_ai_message)
|
| 83 |
|
| 84 |
return final_ai_message
|
| 85 |
|
| 86 |
|
| 87 |
+
async def get_messages_by_chat(db: AsyncSession, chat_id: uuid.UUID) -> list[Message]:
|
| 88 |
"""
|
| 89 |
Retrieves all messages for a specific chat, sorted by creation time
|
| 90 |
to ensure correct conversation order.
|
|
|
|
| 94 |
.where(Message.chat_id == chat_id)
|
| 95 |
.order_by(Message.created_at.asc())
|
| 96 |
)
|
| 97 |
+
return await db.exec(statement).all()
|
db/session.py
CHANGED
|
@@ -1,26 +1,34 @@
|
|
| 1 |
-
from sqlmodel import SQLModel
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
from db.models import Chat, Message
|
| 4 |
from core.config import get_settings
|
| 5 |
|
| 6 |
settings = get_settings()
|
| 7 |
|
| 8 |
-
|
| 9 |
settings.DATABASE_URL,
|
|
|
|
|
|
|
| 10 |
pool_pre_ping=True,
|
| 11 |
-
connect_args=settings.DB_CONNECT_ARGS
|
| 12 |
)
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
"""
|
| 16 |
-
Dependency function to get
|
| 17 |
Ensures the session is always closed after the request.
|
| 18 |
"""
|
| 19 |
-
with
|
| 20 |
yield session
|
| 21 |
|
| 22 |
-
def create_db_and_tables():
|
| 23 |
"""
|
| 24 |
-
Utility function to create database tables.
|
| 25 |
"""
|
| 26 |
-
|
|
|
|
|
|
| 1 |
+
from sqlmodel import SQLModel
|
| 2 |
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
| 3 |
+
from sqlalchemy.orm import sessionmaker
|
| 4 |
|
|
|
|
| 5 |
from core.config import get_settings
|
| 6 |
|
| 7 |
settings = get_settings()
|
| 8 |
|
| 9 |
+
async_engine = create_async_engine(
|
| 10 |
settings.DATABASE_URL,
|
| 11 |
+
echo=False,
|
| 12 |
+
future=True,
|
| 13 |
pool_pre_ping=True,
|
|
|
|
| 14 |
)
|
| 15 |
|
| 16 |
+
# Use sessionmaker for async sessions
|
| 17 |
+
AsyncSessionLocal = sessionmaker(
|
| 18 |
+
autocommit=False, autoflush=False, bind=async_engine, class_=AsyncSession
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
async def get_db():
|
| 22 |
"""
|
| 23 |
+
Dependency function to get an async database session.
|
| 24 |
Ensures the session is always closed after the request.
|
| 25 |
"""
|
| 26 |
+
async with AsyncSessionLocal() as session:
|
| 27 |
yield session
|
| 28 |
|
| 29 |
+
async def create_db_and_tables():
|
| 30 |
"""
|
| 31 |
+
Utility function to create database tables asynchronously.
|
| 32 |
"""
|
| 33 |
+
async with async_engine.begin() as conn:
|
| 34 |
+
await conn.run_sync(SQLModel.metadata.create_all)
|
main.py
CHANGED
|
@@ -12,7 +12,7 @@ settings = get_settings()
|
|
| 12 |
@asynccontextmanager
|
| 13 |
async def lifespan(app: FastAPI):
|
| 14 |
print(f"INFO: Starting up {settings.APP_NAME} v{settings.APP_VERSION}...")
|
| 15 |
-
create_db_and_tables()
|
| 16 |
print("INFO: Database tables checked/created.")
|
| 17 |
yield
|
| 18 |
print(f"INFO: Shutting down {settings.APP_NAME}...")
|
|
|
|
| 12 |
@asynccontextmanager
|
| 13 |
async def lifespan(app: FastAPI):
|
| 14 |
print(f"INFO: Starting up {settings.APP_NAME} v{settings.APP_VERSION}...")
|
| 15 |
+
await create_db_and_tables()
|
| 16 |
print("INFO: Database tables checked/created.")
|
| 17 |
yield
|
| 18 |
print(f"INFO: Shutting down {settings.APP_NAME}...")
|
pyproject.toml
CHANGED
|
@@ -5,6 +5,7 @@ description = "Add your description here"
|
|
| 5 |
readme = "README.md"
|
| 6 |
requires-python = ">=3.11"
|
| 7 |
dependencies = [
|
|
|
|
| 8 |
"fastapi>=0.115.14",
|
| 9 |
"ipykernel>=6.29.5",
|
| 10 |
"langchain>=0.3.26",
|
|
@@ -21,6 +22,7 @@ dependencies = [
|
|
| 21 |
"psycopg2-binary>=2.9.10",
|
| 22 |
"psycopg[binary]>=3.2.9",
|
| 23 |
"pydantic-settings>=2.10.1",
|
|
|
|
| 24 |
"python-dotenv>=1.1.1",
|
| 25 |
"python-jose>=3.5.0",
|
| 26 |
"sqlmodel>=0.0.24",
|
|
|
|
| 5 |
readme = "README.md"
|
| 6 |
requires-python = ">=3.11"
|
| 7 |
dependencies = [
|
| 8 |
+
"asyncpg>=0.30.0",
|
| 9 |
"fastapi>=0.115.14",
|
| 10 |
"ipykernel>=6.29.5",
|
| 11 |
"langchain>=0.3.26",
|
|
|
|
| 22 |
"psycopg2-binary>=2.9.10",
|
| 23 |
"psycopg[binary]>=3.2.9",
|
| 24 |
"pydantic-settings>=2.10.1",
|
| 25 |
+
"python-dateutil>=2.9.0.post0",
|
| 26 |
"python-dotenv>=1.1.1",
|
| 27 |
"python-jose>=3.5.0",
|
| 28 |
"sqlmodel>=0.0.24",
|
uv.lock
CHANGED
|
@@ -178,6 +178,38 @@ wheels = [
|
|
| 178 |
{ url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 },
|
| 179 |
]
|
| 180 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
[[package]]
|
| 182 |
name = "attrs"
|
| 183 |
version = "25.3.0"
|
|
@@ -1312,6 +1344,7 @@ name = "makhfi-ai"
|
|
| 1312 |
version = "0.1.0"
|
| 1313 |
source = { virtual = "." }
|
| 1314 |
dependencies = [
|
|
|
|
| 1315 |
{ name = "fastapi" },
|
| 1316 |
{ name = "ipykernel" },
|
| 1317 |
{ name = "langchain" },
|
|
@@ -1328,6 +1361,7 @@ dependencies = [
|
|
| 1328 |
{ name = "psycopg", extra = ["binary"] },
|
| 1329 |
{ name = "psycopg2-binary" },
|
| 1330 |
{ name = "pydantic-settings" },
|
|
|
|
| 1331 |
{ name = "python-dotenv" },
|
| 1332 |
{ name = "python-jose" },
|
| 1333 |
{ name = "sqlmodel" },
|
|
@@ -1337,6 +1371,7 @@ dependencies = [
|
|
| 1337 |
|
| 1338 |
[package.metadata]
|
| 1339 |
requires-dist = [
|
|
|
|
| 1340 |
{ name = "fastapi", specifier = ">=0.115.14" },
|
| 1341 |
{ name = "ipykernel", specifier = ">=6.29.5" },
|
| 1342 |
{ name = "langchain", specifier = ">=0.3.26" },
|
|
@@ -1353,6 +1388,7 @@ requires-dist = [
|
|
| 1353 |
{ name = "psycopg", extras = ["binary"], specifier = ">=3.2.9" },
|
| 1354 |
{ name = "psycopg2-binary", specifier = ">=2.9.10" },
|
| 1355 |
{ name = "pydantic-settings", specifier = ">=2.10.1" },
|
|
|
|
| 1356 |
{ name = "python-dotenv", specifier = ">=1.1.1" },
|
| 1357 |
{ name = "python-jose", specifier = ">=3.5.0" },
|
| 1358 |
{ name = "sqlmodel", specifier = ">=0.0.24" },
|
|
|
|
| 178 |
{ url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 },
|
| 179 |
]
|
| 180 |
|
| 181 |
+
[[package]]
|
| 182 |
+
name = "asyncpg"
|
| 183 |
+
version = "0.30.0"
|
| 184 |
+
source = { registry = "https://pypi.org/simple" }
|
| 185 |
+
sdist = { url = "https://files.pythonhosted.org/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746 }
|
| 186 |
+
wheels = [
|
| 187 |
+
{ url = "https://files.pythonhosted.org/packages/4c/0e/f5d708add0d0b97446c402db7e8dd4c4183c13edaabe8a8500b411e7b495/asyncpg-0.30.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5e0511ad3dec5f6b4f7a9e063591d407eee66b88c14e2ea636f187da1dcfff6a", size = 674506 },
|
| 188 |
+
{ url = "https://files.pythonhosted.org/packages/6a/a0/67ec9a75cb24a1d99f97b8437c8d56da40e6f6bd23b04e2f4ea5d5ad82ac/asyncpg-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:915aeb9f79316b43c3207363af12d0e6fd10776641a7de8a01212afd95bdf0ed", size = 645922 },
|
| 189 |
+
{ url = "https://files.pythonhosted.org/packages/5c/d9/a7584f24174bd86ff1053b14bb841f9e714380c672f61c906eb01d8ec433/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c198a00cce9506fcd0bf219a799f38ac7a237745e1d27f0e1f66d3707c84a5a", size = 3079565 },
|
| 190 |
+
{ url = "https://files.pythonhosted.org/packages/a0/d7/a4c0f9660e333114bdb04d1a9ac70db690dd4ae003f34f691139a5cbdae3/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3326e6d7381799e9735ca2ec9fd7be4d5fef5dcbc3cb555d8a463d8460607956", size = 3109962 },
|
| 191 |
+
{ url = "https://files.pythonhosted.org/packages/3c/21/199fd16b5a981b1575923cbb5d9cf916fdc936b377e0423099f209e7e73d/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:51da377487e249e35bd0859661f6ee2b81db11ad1f4fc036194bc9cb2ead5056", size = 3064791 },
|
| 192 |
+
{ url = "https://files.pythonhosted.org/packages/77/52/0004809b3427534a0c9139c08c87b515f1c77a8376a50ae29f001e53962f/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc6d84136f9c4d24d358f3b02be4b6ba358abd09f80737d1ac7c444f36108454", size = 3188696 },
|
| 193 |
+
{ url = "https://files.pythonhosted.org/packages/52/cb/fbad941cd466117be58b774a3f1cc9ecc659af625f028b163b1e646a55fe/asyncpg-0.30.0-cp311-cp311-win32.whl", hash = "sha256:574156480df14f64c2d76450a3f3aaaf26105869cad3865041156b38459e935d", size = 567358 },
|
| 194 |
+
{ url = "https://files.pythonhosted.org/packages/3c/0a/0a32307cf166d50e1ad120d9b81a33a948a1a5463ebfa5a96cc5606c0863/asyncpg-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:3356637f0bd830407b5597317b3cb3571387ae52ddc3bca6233682be88bbbc1f", size = 629375 },
|
| 195 |
+
{ url = "https://files.pythonhosted.org/packages/4b/64/9d3e887bb7b01535fdbc45fbd5f0a8447539833b97ee69ecdbb7a79d0cb4/asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e", size = 673162 },
|
| 196 |
+
{ url = "https://files.pythonhosted.org/packages/6e/eb/8b236663f06984f212a087b3e849731f917ab80f84450e943900e8ca4052/asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a", size = 637025 },
|
| 197 |
+
{ url = "https://files.pythonhosted.org/packages/cc/57/2dc240bb263d58786cfaa60920779af6e8d32da63ab9ffc09f8312bd7a14/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3", size = 3496243 },
|
| 198 |
+
{ url = "https://files.pythonhosted.org/packages/f4/40/0ae9d061d278b10713ea9021ef6b703ec44698fe32178715a501ac696c6b/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737", size = 3575059 },
|
| 199 |
+
{ url = "https://files.pythonhosted.org/packages/c3/75/d6b895a35a2c6506952247640178e5f768eeb28b2e20299b6a6f1d743ba0/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a", size = 3473596 },
|
| 200 |
+
{ url = "https://files.pythonhosted.org/packages/c8/e7/3693392d3e168ab0aebb2d361431375bd22ffc7b4a586a0fc060d519fae7/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af", size = 3641632 },
|
| 201 |
+
{ url = "https://files.pythonhosted.org/packages/32/ea/15670cea95745bba3f0352341db55f506a820b21c619ee66b7d12ea7867d/asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e", size = 560186 },
|
| 202 |
+
{ url = "https://files.pythonhosted.org/packages/7e/6b/fe1fad5cee79ca5f5c27aed7bd95baee529c1bf8a387435c8ba4fe53d5c1/asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305", size = 621064 },
|
| 203 |
+
{ url = "https://files.pythonhosted.org/packages/3a/22/e20602e1218dc07692acf70d5b902be820168d6282e69ef0d3cb920dc36f/asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", size = 670373 },
|
| 204 |
+
{ url = "https://files.pythonhosted.org/packages/3d/b3/0cf269a9d647852a95c06eb00b815d0b95a4eb4b55aa2d6ba680971733b9/asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", size = 634745 },
|
| 205 |
+
{ url = "https://files.pythonhosted.org/packages/8e/6d/a4f31bf358ce8491d2a31bfe0d7bcf25269e80481e49de4d8616c4295a34/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", size = 3512103 },
|
| 206 |
+
{ url = "https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4", size = 3592471 },
|
| 207 |
+
{ url = "https://files.pythonhosted.org/packages/67/e4/ab3ca38f628f53f0fd28d3ff20edff1c975dd1cb22482e0061916b4b9a74/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4", size = 3496253 },
|
| 208 |
+
{ url = "https://files.pythonhosted.org/packages/ef/5f/0bf65511d4eeac3a1f41c54034a492515a707c6edbc642174ae79034d3ba/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba", size = 3662720 },
|
| 209 |
+
{ url = "https://files.pythonhosted.org/packages/e7/31/1513d5a6412b98052c3ed9158d783b1e09d0910f51fbe0e05f56cc370bc4/asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590", size = 560404 },
|
| 210 |
+
{ url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623 },
|
| 211 |
+
]
|
| 212 |
+
|
| 213 |
[[package]]
|
| 214 |
name = "attrs"
|
| 215 |
version = "25.3.0"
|
|
|
|
| 1344 |
version = "0.1.0"
|
| 1345 |
source = { virtual = "." }
|
| 1346 |
dependencies = [
|
| 1347 |
+
{ name = "asyncpg" },
|
| 1348 |
{ name = "fastapi" },
|
| 1349 |
{ name = "ipykernel" },
|
| 1350 |
{ name = "langchain" },
|
|
|
|
| 1361 |
{ name = "psycopg", extra = ["binary"] },
|
| 1362 |
{ name = "psycopg2-binary" },
|
| 1363 |
{ name = "pydantic-settings" },
|
| 1364 |
+
{ name = "python-dateutil" },
|
| 1365 |
{ name = "python-dotenv" },
|
| 1366 |
{ name = "python-jose" },
|
| 1367 |
{ name = "sqlmodel" },
|
|
|
|
| 1371 |
|
| 1372 |
[package.metadata]
|
| 1373 |
requires-dist = [
|
| 1374 |
+
{ name = "asyncpg", specifier = ">=0.30.0" },
|
| 1375 |
{ name = "fastapi", specifier = ">=0.115.14" },
|
| 1376 |
{ name = "ipykernel", specifier = ">=6.29.5" },
|
| 1377 |
{ name = "langchain", specifier = ">=0.3.26" },
|
|
|
|
| 1388 |
{ name = "psycopg", extras = ["binary"], specifier = ">=3.2.9" },
|
| 1389 |
{ name = "psycopg2-binary", specifier = ">=2.9.10" },
|
| 1390 |
{ name = "pydantic-settings", specifier = ">=2.10.1" },
|
| 1391 |
+
{ name = "python-dateutil", specifier = ">=2.9.0.post0" },
|
| 1392 |
{ name = "python-dotenv", specifier = ">=1.1.1" },
|
| 1393 |
{ name = "python-jose", specifier = ">=3.5.0" },
|
| 1394 |
{ name = "sqlmodel", specifier = ">=0.0.24" },
|