Aasher commited on
Commit
ce2db9f
·
1 Parent(s): 0cb9b8b

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

Files changed (7) hide show
  1. api/routers/chats.py +20 -20
  2. db/crud/chat.py +21 -29
  3. db/crud/message.py +10 -9
  4. db/session.py +18 -10
  5. main.py +1 -1
  6. pyproject.toml +2 -0
  7. 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 sqlmodel import Session
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: Session) -> Chat:
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: Session = Depends(get_db), user_id: uuid.UUID = Depends(get_current_user)):
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: Session = Depends(get_db), user_id: uuid.UUID = Depends(get_current_user)):
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: Session = Depends(get_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: Session = Depends(get_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: Session = Depends(get_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: Session = 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 = 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: Session, user_id: uuid.UUID) -> Chat:
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: Session, user_id: uuid.UUID) -> list[Chat]:
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
- return db.exec(statement).all()
 
25
 
26
- def get_chat_by_id(db: Session, chat_id: uuid.UUID) -> Optional[Chat]:
27
- """
28
- Retrieves a single chat by its unique ID.
29
- Returns None if the chat does not exist.
30
- """
31
- return db.get(Chat, chat_id)
32
 
33
- def update_chat_title(db: Session, chat: Chat, chat_update: ChatUpdate) -> Chat:
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: Session, chat: Chat) -> None:
45
- """
46
- Deletes a chat from the database.
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 Session, select
 
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: Session,
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: Session, chat_id: uuid.UUID) -> list[Message]:
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, create_engine, Session
 
 
2
 
3
- from db.models import Chat, Message
4
  from core.config import get_settings
5
 
6
  settings = get_settings()
7
 
8
- engine = create_engine(
9
  settings.DATABASE_URL,
 
 
10
  pool_pre_ping=True,
11
- connect_args=settings.DB_CONNECT_ARGS
12
  )
13
 
14
- def get_db():
 
 
 
 
 
15
  """
16
- Dependency function to get a database session.
17
  Ensures the session is always closed after the request.
18
  """
19
- with Session(engine) as session:
20
  yield session
21
 
22
- def create_db_and_tables():
23
  """
24
- Utility function to create database tables.
25
  """
26
- SQLModel.metadata.create_all(engine)
 
 
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" },