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
36 changes: 19 additions & 17 deletions alembic/versions/0002_seed_roles_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,25 @@
catalog has a single source of truth.

"""
from datetime import datetime
from typing import Sequence, Union

import uuid
from collections.abc import Sequence
from datetime import UTC, datetime

from alembic import op
import sqlalchemy as sa

from alembic import op
from db.seed import BUILTIN_ROLES, PERMISSIONS, ROLE_PERMISSION_MAP, permission_name


revision: str = "0002_seed_roles_permissions"
down_revision: Union[str, Sequence[str], None] = "0001_initial"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
down_revision: str | Sequence[str] | None = "0001_initial"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
bind = op.get_bind()
now = datetime.utcnow()
now = datetime.now(UTC)

# Permissions
perms_table = sa.table(
Expand Down Expand Up @@ -94,8 +94,7 @@ def upgrade() -> None:
for row in bind.execute(sa.text("SELECT id, name FROM permissions")).fetchall()
}
role_id_by_name = {
row[1]: row[0]
for row in bind.execute(sa.text("SELECT id, name FROM roles")).fetchall()
row[1]: row[0] for row in bind.execute(sa.text("SELECT id, name FROM roles")).fetchall()
}
existing_rp = {
(row[0], row[1])
Expand Down Expand Up @@ -132,19 +131,22 @@ def downgrade() -> None:
perm_names = [permission_name(r, a) for r, a, _ in PERMISSIONS]
if role_names:
bind.execute(
sa.text("DELETE FROM role_permissions WHERE role_id IN "
"(SELECT id FROM roles WHERE name IN :names)")
.bindparams(sa.bindparam("names", expanding=True)),
sa.text(
"DELETE FROM role_permissions WHERE role_id IN "
"(SELECT id FROM roles WHERE name IN :names)"
).bindparams(sa.bindparam("names", expanding=True)),
{"names": role_names},
)
bind.execute(
sa.text("DELETE FROM roles WHERE name IN :names AND is_system = 1")
.bindparams(sa.bindparam("names", expanding=True)),
sa.text("DELETE FROM roles WHERE name IN :names AND is_system = 1").bindparams(
sa.bindparam("names", expanding=True)
),
{"names": role_names},
)
if perm_names:
bind.execute(
sa.text("DELETE FROM permissions WHERE name IN :names")
.bindparams(sa.bindparam("names", expanding=True)),
sa.text("DELETE FROM permissions WHERE name IN :names").bindparams(
sa.bindparam("names", expanding=True)
),
{"names": perm_names},
)
12 changes: 6 additions & 6 deletions src/api/knowledge_filter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
import uuid
from datetime import datetime
from datetime import UTC, datetime
from typing import Any

from fastapi import Depends, Request
Expand Down Expand Up @@ -128,8 +128,8 @@ async def create_knowledge_filter(
"owner": user.user_id,
"allowed_users": body.allowedUsers,
"allowed_groups": body.allowedGroups,
"created_at": datetime.utcnow().isoformat(),
"updated_at": datetime.utcnow().isoformat(),
"created_at": datetime.now(UTC).isoformat(),
"updated_at": datetime.now(UTC).isoformat(),
}

result = await knowledge_filter_service.create_knowledge_filter(
Expand Down Expand Up @@ -225,7 +225,7 @@ async def update_knowledge_filter(
if body.allowedGroups is not None
else existing_filter.get("allowed_groups", []),
"created_at": existing_filter["created_at"],
"updated_at": datetime.utcnow().isoformat(),
"updated_at": datetime.now(UTC).isoformat(),
}

result = await knowledge_filter_service.create_knowledge_filter(
Expand Down Expand Up @@ -291,7 +291,7 @@ async def subscribe_to_knowledge_filter(
"subscription_id": monitor_result["subscription_id"],
"monitor_id": monitor_result["monitor_id"],
"webhook_url": monitor_result["webhook_url"],
"created_at": datetime.utcnow().isoformat(),
"created_at": datetime.now(UTC).isoformat(),
"notification_config": body.notification_config or {},
}

Expand Down Expand Up @@ -420,7 +420,7 @@ async def knowledge_filter_webhook(
"filter_id": filter_id,
"subscription_id": subscription_id,
"matched_documents": len(matched_documents),
"timestamp": datetime.utcnow().isoformat(),
"timestamp": datetime.now(UTC).isoformat(),
}
)

Expand Down
5 changes: 3 additions & 2 deletions src/api/settings/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
1388–1455). No behavior change.
"""

from datetime import UTC
from typing import Any

from fastapi.responses import JSONResponse
Expand Down Expand Up @@ -205,8 +206,8 @@ async def _create_openrag_docs_filter(knowledge_filter_service, session_manager,
"owner": owner_user_id,
"allowed_users": [],
"allowed_groups": [],
"created_at": datetime.utcnow().isoformat(),
"updated_at": datetime.utcnow().isoformat(),
"created_at": datetime.now(UTC).isoformat(),
"updated_at": datetime.now(UTC).isoformat(),
}

result = await knowledge_filter_service.create_knowledge_filter(
Expand Down
2 changes: 1 addition & 1 deletion src/connectors/onedrive/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ def _get_subscription_expiry(self) -> str:
"""Get subscription expiry time (Graph caps duration; often <= 3 days)."""
from datetime import datetime, timedelta

expiry = datetime.utcnow() + timedelta(days=3)
expiry = datetime.now(UTC) + timedelta(days=3)
return expiry.strftime("%Y-%m-%dT%H:%M:%S.%fZ")

async def list_files(
Expand Down
2 changes: 1 addition & 1 deletion src/connectors/sharepoint/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ def _get_subscription_expiry(self) -> str:
"""Get subscription expiry time (max 3 days for Graph API)"""
from datetime import datetime, timedelta

expiry = datetime.utcnow() + timedelta(days=3) # 3 days max for Graph
expiry = datetime.now(UTC) + timedelta(days=3) # 3 days max for Graph
return expiry.strftime("%Y-%m-%dT%H:%M:%S.%fZ")

def _parse_sharepoint_url(self) -> dict[str, str] | None:
Expand Down
49 changes: 17 additions & 32 deletions src/db/migrations_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import json
import os
from collections.abc import Iterable
from datetime import datetime
from datetime import UTC, datetime

from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
Expand Down Expand Up @@ -53,7 +53,7 @@ async def _already_done(session: AsyncSession, name: str) -> bool:


async def _mark_done(session: AsyncSession, name: str, notes: str = "") -> None:
session.add(MigrationStatus(name=name, completed_at=datetime.utcnow(), notes=notes))
session.add(MigrationStatus(name=name, completed_at=datetime.now(UTC), notes=notes))
await session.flush()


Expand Down Expand Up @@ -194,6 +194,17 @@ async def migrate_config_yaml_to_db(session: AsyncSession) -> int:
return written


def _parse_legacy_dt(value: str | None) -> datetime | None:
"""Parse a legacy ISO datetime string, coercing naive values to UTC."""
if not value:
return None
try:
dt = datetime.fromisoformat(value)
except (TypeError, ValueError):
return None
return dt if dt.tzinfo is not None else dt.replace(tzinfo=UTC)


async def migrate_chat_history_json_to_db(session: AsyncSession) -> dict[str, int]:
"""Copy ``data/session_ownership.json`` and ``data/conversations.json``
into the DB. Idempotent — only inserts rows that aren't already present.
Expand All @@ -214,20 +225,8 @@ async def migrate_chat_history_json_to_db(session: AsyncSession) -> dict[str, in
uid = data.get("user_id")
if not uid:
continue
try:
created = (
datetime.fromisoformat(data["created_at"]) if data.get("created_at") else None
)
except Exception: # noqa: BLE001
created = None
try:
last = (
datetime.fromisoformat(data["last_accessed"])
if data.get("last_accessed")
else None
)
except Exception: # noqa: BLE001
last = None
created = _parse_legacy_dt(data.get("created_at"))
last = _parse_legacy_dt(data.get("last_accessed"))
inserted = await repo.upsert_raw(
response_id=str(sid),
user_id=str(uid),
Expand All @@ -249,22 +248,8 @@ async def migrate_chat_history_json_to_db(session: AsyncSession) -> dict[str, in
continue
if await crepo.get(str(resp_id)) is not None:
continue
try:
created = (
datetime.fromisoformat(meta["created_at"])
if meta.get("created_at")
else None
)
except Exception: # noqa: BLE001
created = None
try:
last = (
datetime.fromisoformat(meta["last_activity"])
if meta.get("last_activity")
else None
)
except Exception: # noqa: BLE001
last = None
created = _parse_legacy_dt(meta.get("created_at"))
last = _parse_legacy_dt(meta.get("last_activity"))
await crepo.upsert(
response_id=str(resp_id),
user_id=str(uid),
Expand Down
11 changes: 5 additions & 6 deletions src/db/models/api_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
OpenSearch-backed APIKeyService is unchanged.
"""

from datetime import datetime
from typing import Optional
from datetime import UTC, datetime

from sqlalchemy import JSON, Column
from sqlmodel import Field, SQLModel
Expand All @@ -19,10 +18,10 @@ class ApiKey(SQLModel, table=True):
name: str = Field(max_length=128)
key_hash: str = Field(max_length=128, unique=True, index=True)
key_prefix: str = Field(max_length=32)
scope_role_ids: Optional[list] = Field(
scope_role_ids: list | None = Field(
default=None, sa_column=Column("scope_role_ids", JSON, nullable=True)
)
last_used_at: Optional[datetime] = Field(default=None)
created_at: datetime = Field(default_factory=datetime.utcnow)
revoked_at: Optional[datetime] = Field(default=None)
last_used_at: datetime | None = Field(default=None)
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
revoked_at: datetime | None = Field(default=None)
revoked: bool = Field(default=False)
19 changes: 9 additions & 10 deletions src/db/models/audit_log.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from datetime import datetime
from typing import Optional
from datetime import UTC, datetime

from sqlalchemy import JSON, Column
from sqlmodel import Field, SQLModel
Expand All @@ -9,16 +8,16 @@ class AuditLog(SQLModel, table=True):
__tablename__ = "audit_log"

id: str = Field(primary_key=True, max_length=64)
ts: datetime = Field(default_factory=datetime.utcnow, index=True)
actor_user_id: Optional[str] = Field(
ts: datetime = Field(default_factory=lambda: datetime.now(UTC), index=True)
actor_user_id: str | None = Field(
default=None, foreign_key="users.id", max_length=64, index=True
)
actor_api_key_id: Optional[str] = Field(default=None, max_length=64)
actor_api_key_id: str | None = Field(default=None, max_length=64)
event: str = Field(max_length=128, index=True)
target_type: Optional[str] = Field(default=None, max_length=64)
target_id: Optional[str] = Field(default=None, max_length=128)
audit_metadata: Optional[dict] = Field(
target_type: str | None = Field(default=None, max_length=64)
target_id: str | None = Field(default=None, max_length=128)
audit_metadata: dict | None = Field(
default=None, sa_column=Column("metadata", JSON, nullable=True)
)
ip: Optional[str] = Field(default=None, max_length=64)
user_agent: Optional[str] = Field(default=None, max_length=512)
ip: str | None = Field(default=None, max_length=64)
user_agent: str | None = Field(default=None, max_length=512)
Comment on lines +11 to +23

@coderabbitai coderabbitai Bot Jul 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for timezone=True in model definitions
rg -n 'timezone.*True' src/db/models/ --type=py

# Check for custom SQLAlchemy DateTime types
rg -n 'class.*DateTime' src/db/ --type=py

# Check for database URL configuration
rg -n 'DATABASE_URL|database_url|db_url' src/config/ --type=py

# Check if timezone is used beyond imports in all changed files
for f in src/db/models/audit_log.py src/db/models/conversation.py src/db/models/role.py src/db/models/workspace_config.py src/db/repositories/api_key_repo.py src/db/repositories/conversation_repo.py src/db/repositories/workspace_config_repo.py; do
  echo "=== $f ==="
  rg -n 'timezone' "$f" | rg -v 'from datetime import'
done

Repository: langflow-ai/openrag

Length of output: 448


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- audit_log.py outline ---'
ast-grep outline src/db/models/audit_log.py --view expanded || true

echo '--- audit_log.py lines ---'
nl -ba src/db/models/audit_log.py | sed -n '1,220p'

echo '--- other model files with timestamp fields ---'
for f in src/db/models/conversation.py src/db/models/role.py src/db/models/workspace_config.py; do
  echo "=== $f ==="
  nl -ba "$f" | sed -n '1,220p'
done

echo '--- search for DateTime/Timezone column declarations ---'
rg -n 'DateTime|timezone\s*=' src -g '*.py' || true

echo '--- search for engine/backend config ---'
rg -n 'create_engine|sqlite|postgres|postgresql|mysql|DATABASE_URL|database_url|db_url|engine' src -g '*.py' || true

Repository: langflow-ai/openrag

Length of output: 960


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- audit_log.py ---'
sed -n '1,220p' src/db/models/audit_log.py

echo '--- conversation.py ---'
sed -n '1,220p' src/db/models/conversation.py

echo '--- role.py ---'
sed -n '1,220p' src/db/models/role.py

echo '--- workspace_config.py ---'
sed -n '1,260p' src/db/models/workspace_config.py

echo '--- explicit DateTime/timezone usage in src ---'
rg -n 'DateTime|timezone\s*=' src -g '*.py' || true

echo '--- DB/backend config mentions ---'
rg -n 'DATABASE_URL|database_url|db_url|sqlite|postgresql|postgres|mysql|create_engine|engine' src -g '*.py' || true

Repository: langflow-ai/openrag

Length of output: 12999


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- timestamp field usages ---'
rg -n '\b(ts|created_at|last_activity|updated_at)\b' src -g '*.py' || true

echo '--- datetime comparisons / UTC now usage ---'
rg -n 'datetime\.now\(UTC\)|UTC\)|<|>|<=|>=|replace\(tzinfo|astimezone|timezone' src/db src/services src/app -g '*.py' || true

echo '--- db engine file ---'
sed -n '1,220p' src/db/engine.py

Repository: langflow-ai/openrag

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- timestamp field usages ---'
rg -n '\b(ts|created_at|last_activity|updated_at)\b' src -g '*.py' || true

echo '--- datetime comparisons / UTC now usage ---'
rg -n 'datetime\.now\(UTC\)|astimezone|replace\(tzinfo|timezone' src/db src/services src/app -g '*.py' || true

echo '--- db engine file ---'
sed -n '1,220p' src/db/engine.py

Repository: langflow-ai/openrag

Length of output: 20789


Use timezone-aware columns for the UTC timestamp fields
In src/db/models/audit_log.py and the other timestamp models in this PR, datetime.now(UTC) still maps to the default naive DateTime. Add timezone=True on those DB-backed timestamp columns so rows round-trip with tzinfo intact and don’t diverge on serialization or comparisons.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/db/models/audit_log.py` around lines 12 - 24, Update the timestamp field
`ts` in the audit log model and all other timestamp model fields introduced in
this PR to use timezone-aware database columns by configuring their SQLAlchemy
`DateTime` type with `timezone=True`, while retaining the UTC-aware default
factory such as `datetime.now(UTC)`.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be out of scope for this PR

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Vchen7629, that's fair — adding timezone=True to all the DB-backed timestamp columns is a broader change than what this PR set out to do (which is fixing the datetime.utcnow() removal in Python 3.13). I'll leave it as-is here.

Would you like me to open a follow-up issue to track adding timezone=True to the timestamp columns, so it doesn't get lost?

19 changes: 8 additions & 11 deletions src/db/models/conversation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,22 @@
ids not in the users table yet.
"""

from datetime import datetime
from typing import Optional
from datetime import UTC, datetime

from sqlalchemy import Index
from sqlmodel import Field, SQLModel


class Conversation(SQLModel, table=True):
__tablename__ = "conversations"
__table_args__ = (
Index("ix_conversations_user_recent", "user_id", "last_activity"),
)
__table_args__ = (Index("ix_conversations_user_recent", "user_id", "last_activity"),)

response_id: str = Field(primary_key=True, max_length=64)
user_id: str = Field(max_length=64, index=True)
title: Optional[str] = Field(default=None, max_length=512)
endpoint: Optional[str] = Field(default=None, max_length=64)
previous_response_id: Optional[str] = Field(default=None, max_length=64)
filter_id: Optional[str] = Field(default=None, max_length=128)
title: str | None = Field(default=None, max_length=512)
endpoint: str | None = Field(default=None, max_length=64)
previous_response_id: str | None = Field(default=None, max_length=64)
filter_id: str | None = Field(default=None, max_length=128)
total_messages: int = Field(default=0)
created_at: datetime = Field(default_factory=datetime.utcnow)
last_activity: datetime = Field(default_factory=datetime.utcnow)
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
last_activity: datetime = Field(default_factory=lambda: datetime.now(UTC))
4 changes: 2 additions & 2 deletions src/db/models/migration_status.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Tracks one-shot runtime migrations (e.g. JSON->DB)."""

from datetime import datetime
from datetime import UTC, datetime

from sqlmodel import Field, SQLModel

Expand All @@ -9,5 +9,5 @@ class MigrationStatus(SQLModel, table=True):
__tablename__ = "migration_status"

name: str = Field(primary_key=True, max_length=128)
completed_at: datetime = Field(default_factory=datetime.utcnow)
completed_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
notes: str = Field(default="", max_length=2048)
9 changes: 4 additions & 5 deletions src/db/models/role.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from datetime import datetime
from typing import Optional
from datetime import UTC, datetime

from sqlmodel import Field, SQLModel

Expand All @@ -9,7 +8,7 @@ class Role(SQLModel, table=True):

id: str = Field(primary_key=True, max_length=64)
name: str = Field(max_length=64, unique=True, index=True)
description: Optional[str] = Field(default=None, max_length=512)
description: str | None = Field(default=None, max_length=512)
is_system: bool = Field(default=False)
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
7 changes: 3 additions & 4 deletions src/db/models/session_ownership.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
violations.
"""

from datetime import datetime
from typing import Optional
from datetime import UTC, datetime

from sqlmodel import Field, SQLModel

Expand All @@ -20,5 +19,5 @@ class SessionOwnership(SQLModel, table=True):

response_id: str = Field(primary_key=True, max_length=64)
user_id: str = Field(max_length=64, index=True)
created_at: datetime = Field(default_factory=datetime.utcnow)
last_accessed: Optional[datetime] = Field(default=None)
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
last_accessed: datetime | None = Field(default=None)
Loading
Loading