Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions alembic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@

2\. Gere o script de migração:
```bash
alembic revision --autogenerate -m "alguma mensagem descritiva"
uv run alembic revision --autogenerate -m "alguma mensagem descritiva"
```

> [!WARNING]
> Revise cuidadosamente o script de migração antes de executá-lo. Ajustes manuais podem ser necessários para garantir que ele funcione corretamente.

3\. Execute o script de migração:
```bash
alembic upgrade head
uv run alembic upgrade head
```

> [!WARNING]
Expand Down
108 changes: 108 additions & 0 deletions alembic/versions/c0135b4524b9_use_timezone_aware_timestamps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""Use timezone-aware timestamps.

Revision ID: c0135b4524b9
Revises: 091db36ec6e3
Create Date: 2026-02-05 15:55:43.825104
"""

from typing import Sequence, Union

import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision: str = "c0135b4524b9"
down_revision: Union[str, Sequence[str], None] = "091db36ec6e3"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""

# Threads table
op.alter_column(
"thread",
"created_at",
existing_type=postgresql.TIMESTAMP(),
type_=sa.TIMESTAMP(timezone=True),
existing_nullable=False,
)

# Messages table
op.alter_column(
"message",
"created_at",
existing_type=postgresql.TIMESTAMP(),
type_=sa.TIMESTAMP(timezone=True),
existing_nullable=False,
)

# Feedbacks table
op.alter_column(
"feedback",
"created_at",
existing_type=postgresql.TIMESTAMP(),
type_=sa.TIMESTAMP(timezone=True),
existing_nullable=False,
)
op.alter_column(
"feedback",
"updated_at",
existing_type=postgresql.TIMESTAMP(),
type_=sa.TIMESTAMP(timezone=True),
existing_nullable=True,
)
op.alter_column(
"feedback",
"synced_at",
existing_type=postgresql.TIMESTAMP(),
type_=sa.TIMESTAMP(timezone=True),
existing_nullable=True,
)


def downgrade() -> None:
"""Downgrade schema."""

# Feedbacks table
op.alter_column(
"feedback",
"synced_at",
existing_type=sa.TIMESTAMP(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True,
)
op.alter_column(
"feedback",
"updated_at",
existing_type=sa.TIMESTAMP(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=True,
)
op.alter_column(
"feedback",
"created_at",
existing_type=sa.TIMESTAMP(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=False,
)

# Messages table
op.alter_column(
"message",
"created_at",
existing_type=sa.TIMESTAMP(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=False,
)

# Threads table
op.alter_column(
"thread",
"created_at",
existing_type=sa.TIMESTAMP(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=False,
)
4 changes: 2 additions & 2 deletions app/api/dependencies/feedback.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime
from datetime import datetime, timezone
from typing import Annotated

from fastapi import Depends
Expand Down Expand Up @@ -92,7 +92,7 @@ def send_feedback(
sync_status = (
FeedbackSyncStatus.SUCCESS if success else FeedbackSyncStatus.FAILED
)
synced_at = datetime.now()
synced_at = datetime.now(timezone.utc)

return sync_status, synced_at

Expand Down
9 changes: 6 additions & 3 deletions app/db/database.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime
from datetime import datetime, timezone
from typing import TypeVar
from uuid import UUID

Expand All @@ -24,7 +24,10 @@

T = TypeVar("T", bound=SQLModel)

engine = create_async_engine(settings.SQLALCHEMY_DB_URL)
engine = create_async_engine(
url=settings.SQLALCHEMY_DB_URL,
connect_args={"options": "-c timezone=utc"},
)

sessionmaker = async_sessionmaker(engine, expire_on_commit=False)

Expand Down Expand Up @@ -215,7 +218,7 @@ async def upsert_feedback(
)

db_feedback.sqlmodel_update(feedback_data)
db_feedback.updated_at = datetime.now()
db_feedback.updated_at = datetime.now(timezone.utc)
db_feedback.sync_status = FeedbackSyncStatus.PENDING
self.session.add(db_feedback)
await self.session.commit()
Expand Down
31 changes: 24 additions & 7 deletions app/db/models.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import uuid
from datetime import datetime
from datetime import datetime, timezone
from enum import Enum

from pydantic import JsonValue
from sqlalchemy import Enum as SAEnum
from sqlmodel import JSON, Column, Field, Integer, Relationship, SQLModel
from sqlmodel import JSON, TIMESTAMP, Column, Field, Integer, Relationship, SQLModel


# =============================================================================
Expand All @@ -20,7 +20,11 @@ class ThreadCreate(ThreadPayload):

class Thread(ThreadCreate, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
created_at: datetime = Field(default_factory=datetime.now, index=True)
created_at: datetime = Field(
default_factory=lambda: datetime.now(timezone.utc),
sa_type=TIMESTAMP(timezone=True),
index=True,
)
deleted: bool = Field(default=False)

messages: list["Message"] = Relationship(back_populates="thread")
Expand Down Expand Up @@ -61,7 +65,11 @@ class MessageCreate(SQLModel):


class Message(MessageCreate, table=True):
created_at: datetime = Field(default_factory=datetime.now, index=True)
created_at: datetime = Field(
default_factory=lambda: datetime.now(timezone.utc),
sa_type=TIMESTAMP(timezone=True),
index=True,
)

thread: Thread = Relationship(back_populates="messages")
feedback: "Feedback" = Relationship(back_populates="message")
Expand Down Expand Up @@ -98,15 +106,24 @@ class FeedbackPublic(FeedbackCreate):

class Feedback(FeedbackCreate, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
created_at: datetime = Field(default_factory=datetime.now)
updated_at: datetime | None = Field(default=None)
created_at: datetime = Field(
default_factory=lambda: datetime.now(timezone.utc),
sa_type=TIMESTAMP(timezone=True),
)
updated_at: datetime | None = Field(
default=None,
sa_type=TIMESTAMP(timezone=True),
)
sync_status: FeedbackSyncStatus = Field(
sa_column=Column(
SAEnum(FeedbackSyncStatus),
nullable=False,
),
default=FeedbackSyncStatus.PENDING,
)
synced_at: datetime | None = Field(default=None)
synced_at: datetime | None = Field(
default=None,
sa_type=TIMESTAMP(timezone=True),
)

message: Message = Relationship(back_populates="feedback")
4 changes: 2 additions & 2 deletions tests/app/api/routers/test_chatbot.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import uuid
from contextlib import asynccontextmanager
from datetime import datetime
from datetime import datetime, timezone

import jwt
import pytest
Expand All @@ -26,7 +26,7 @@

class MockLangSmithFeedbackSender:
def send_feedback(self, feedback: Feedback, created: bool):
return FeedbackSyncStatus.SUCCESS, datetime.now()
return FeedbackSyncStatus.SUCCESS, datetime.now(timezone.utc)


class MockReActAgent:
Expand Down
6 changes: 3 additions & 3 deletions tests/app/db/test_database.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import uuid
from datetime import datetime
from datetime import datetime, timezone

from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncEngine
Expand Down Expand Up @@ -243,7 +243,7 @@ async def test_update_feedback_sync_status_success(
self, database: AsyncDatabase, feedback: Feedback
):
"""Test updating feedback sync status."""
synced_at = datetime.now()
synced_at = datetime.now(timezone.utc)

feedback_synced = await database.update_feedback_sync_status(
feedback_id=feedback.id,
Expand All @@ -260,7 +260,7 @@ async def test_update_feedback_sync_status_not_found(self, database: AsyncDataba
feedback_synced = await database.update_feedback_sync_status(
feedback_id=uuid.uuid4(),
sync_status=FeedbackSyncStatus.SUCCESS,
synced_at=datetime.now(),
synced_at=datetime.now(timezone.utc),
)

assert feedback_synced is None
4 changes: 3 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ async def async_engine(
) -> AsyncGenerator[AsyncEngine, None]:
"""Create an async engine connected to the test PostgreSQL container."""
postgres_url = postgres_container.get_connection_url()
engine = create_async_engine(postgres_url, echo=False)
engine = create_async_engine(
url=postgres_url, connect_args={"options": "-c timezone=utc"}
)
yield engine
await engine.dispose()

Expand Down