Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion backend/app/api/routes/admin/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ async def replay_events(
"""Replay events by filter criteria, with optional dry-run mode."""
replay_id = f"replay-{uuid4().hex}"
result = await service.prepare_or_schedule_replay(
replay_filter=ReplayFilter.model_validate(request),
replay_filter=ReplayFilter(**request.model_dump(include=set(ReplayFilter.__dataclass_fields__))),
dry_run=request.dry_run,
replay_id=replay_id,
target_service=request.target_service,
Expand Down
2 changes: 1 addition & 1 deletion backend/app/api/routes/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ async def update_user(
if not existing_user:
raise HTTPException(status_code=404, detail="User not found")

domain_update = DomainUserUpdate.model_validate(user_update)
domain_update = DomainUserUpdate(**user_update.model_dump())

updated_user = await admin_user_service.update_user(
admin_user_id=admin.user_id, user_id=user_id, update=domain_update
Expand Down
2 changes: 1 addition & 1 deletion backend/app/api/routes/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ async def update_subscription(
notification_service: FromDishka[NotificationService],
) -> NotificationSubscription:
"""Update subscription settings for a notification channel."""
update_data = DomainSubscriptionUpdate.model_validate(subscription)
update_data = DomainSubscriptionUpdate(**subscription.model_dump())
updated_sub = await notification_service.update_subscription(
user_id=user.user_id,
channel=channel,
Expand Down
3 changes: 2 additions & 1 deletion backend/app/api/routes/replay.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ async def create_replay_session(
service: FromDishka[EventReplayService],
) -> ReplayResponse:
"""Create a new event replay session from a configuration."""
result = await service.create_session_from_config(ReplayConfig.model_validate(replay_request))
config_fields = set(ReplayConfig.__dataclass_fields__)
result = await service.create_session_from_config(ReplayConfig(**replay_request.model_dump(include=config_fields)))
return ReplayResponse.model_validate(result)


Expand Down
4 changes: 2 additions & 2 deletions backend/app/api/routes/saved_scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ async def create_saved_script(
saved_script_service: FromDishka[SavedScriptService],
) -> SavedScriptResponse:
"""Save a new script to the user's collection."""
create = DomainSavedScriptCreate.model_validate(saved_script)
create = DomainSavedScriptCreate(**saved_script.model_dump())
domain = await saved_script_service.create_saved_script(create, user.user_id)
return SavedScriptResponse.model_validate(domain)

Expand Down Expand Up @@ -68,7 +68,7 @@ async def update_saved_script(
saved_script_service: FromDishka[SavedScriptService],
) -> SavedScriptResponse:
"""Update an existing saved script."""
update_data = DomainSavedScriptUpdate.model_validate(script_update)
update_data = DomainSavedScriptUpdate(**script_update.model_dump())
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Feb 14, 2026

Choose a reason for hiding this comment

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

P2: model_dump() includes optional fields with None defaults, so the update payload will always carry None for omitted fields and a new updated_at. That can overwrite existing values unintentionally. Use model_dump(exclude_unset=True) when building the dataclass so only client-provided fields are updated.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/app/api/routes/saved_scripts.py, line 71:

<comment>`model_dump()` includes optional fields with `None` defaults, so the update payload will always carry `None` for omitted fields and a new `updated_at`. That can overwrite existing values unintentionally. Use `model_dump(exclude_unset=True)` when building the dataclass so only client-provided fields are updated.</comment>

<file context>
@@ -68,7 +68,7 @@ async def update_saved_script(
 ) -> SavedScriptResponse:
     """Update an existing saved script."""
-    update_data = DomainSavedScriptUpdate.model_validate(script_update)
+    update_data = DomainSavedScriptUpdate(**script_update.model_dump())
     domain = await saved_script_service.update_saved_script(script_id, user.user_id, update_data)
     return SavedScriptResponse.model_validate(domain)
</file context>
Suggested change
update_data = DomainSavedScriptUpdate(**script_update.model_dump())
update_data = DomainSavedScriptUpdate(**script_update.model_dump(exclude_unset=True))
Fix with Cubic

domain = await saved_script_service.update_saved_script(script_id, user.user_id, update_data)
return SavedScriptResponse.model_validate(domain)

Expand Down
6 changes: 3 additions & 3 deletions backend/app/api/routes/user_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ async def update_user_settings(
settings_service: FromDishka[UserSettingsService],
) -> UserSettings:
"""Update the authenticated user's settings."""
domain_updates = DomainUserSettingsUpdate.model_validate(updates)
domain_updates = DomainUserSettingsUpdate(**updates.model_dump())
domain = await settings_service.update_user_settings(current_user.user_id, domain_updates)
return UserSettings.model_validate(domain)

Expand All @@ -68,7 +68,7 @@ async def update_notification_settings(
"""Update notification preferences."""
domain = await settings_service.update_notification_settings(
current_user.user_id,
DomainNotificationSettings.model_validate(notifications),
DomainNotificationSettings(**notifications.model_dump()),
)
return UserSettings.model_validate(domain)

Expand All @@ -82,7 +82,7 @@ async def update_editor_settings(
"""Update code editor preferences."""
domain = await settings_service.update_editor_settings(
current_user.user_id,
DomainEditorSettings.model_validate(editor),
DomainEditorSettings(**editor.model_dump()),
)
return UserSettings.model_validate(domain)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import dataclasses
from datetime import datetime, timedelta, timezone
from typing import Any

Expand Down Expand Up @@ -211,7 +212,7 @@ async def archive_event(self, event: DomainEvent, deleted_by: str) -> bool:
return True

async def update_replay_session(self, session_id: str, updates: ReplaySessionUpdate) -> bool:
update_dict = updates.model_dump(exclude_none=True)
update_dict = {k: v for k, v in dataclasses.asdict(updates).items() if v is not None}
if not update_dict:
return False
doc = await ReplaySessionDocument.find_one(ReplaySessionDocument.session_id == session_id)
Expand Down
13 changes: 7 additions & 6 deletions backend/app/db/repositories/admin/admin_user_repository.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import dataclasses
import re
from datetime import datetime, timezone

Expand Down Expand Up @@ -28,9 +29,9 @@
class AdminUserRepository:

async def create_user(self, create_data: DomainUserCreate) -> User:
doc = UserDocument(**create_data.model_dump())
doc = UserDocument(**dataclasses.asdict(create_data))
await doc.insert()
return User.model_validate(doc)
return User(**doc.model_dump(include=set(User.__dataclass_fields__)))

async def list_users(
self, limit: int = 100, offset: int = 0, search: str | None = None, role: UserRole | None = None
Expand All @@ -52,26 +53,26 @@ async def list_users(
query = UserDocument.find(*conditions)
total = await query.count()
docs = await query.skip(offset).limit(limit).to_list()
users = [User.model_validate(doc) for doc in docs]
users = [User(**doc.model_dump(include=set(User.__dataclass_fields__))) for doc in docs]
return UserListResult(users=users, total=total, offset=offset, limit=limit)

async def get_user_by_id(self, user_id: str) -> User | None:
doc = await UserDocument.find_one(UserDocument.user_id == user_id)
return User.model_validate(doc) if doc else None
return User(**doc.model_dump(include=set(User.__dataclass_fields__))) if doc else None

async def update_user(self, user_id: str, update_data: UserUpdate) -> User | None:
doc = await UserDocument.find_one(UserDocument.user_id == user_id)
if not doc:
return None

update_dict = update_data.model_dump(exclude_none=True)
update_dict = {k: v for k, v in dataclasses.asdict(update_data).items() if v is not None}
if "password" in update_dict:
update_dict["hashed_password"] = update_dict.pop("password")

if update_dict:
update_dict["updated_at"] = datetime.now(timezone.utc)
await doc.set(update_dict)
return User.model_validate(doc)
return User(**doc.model_dump(include=set(User.__dataclass_fields__)))

async def delete_user(self, user_id: str, cascade: bool = True) -> UserDeleteResult:
doc = await UserDocument.find_one(UserDocument.user_id == user_id)
Expand Down
2 changes: 1 addition & 1 deletion backend/app/db/repositories/dlq_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
DLQMessageListResult,
DLQMessageStatus,
DLQMessageUpdate,
DLQTopicSummary,
)
from app.domain.enums import EventType
from app.schemas_pydantic.dlq import DLQTopicSummary


class DLQRepository:
Expand Down
9 changes: 5 additions & 4 deletions backend/app/db/repositories/execution_repository.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import dataclasses
from datetime import datetime, timezone
from typing import Any

Expand All @@ -17,11 +18,11 @@ def __init__(self, logger: structlog.stdlib.BoundLogger):
self.logger = logger

async def create_execution(self, create_data: DomainExecutionCreate) -> DomainExecution:
doc = ExecutionDocument(**create_data.model_dump())
doc = ExecutionDocument(**dataclasses.asdict(create_data))
self.logger.info("Inserting execution into MongoDB", execution_id=doc.execution_id)
await doc.insert()
self.logger.info("Inserted execution", execution_id=doc.execution_id)
return DomainExecution.model_validate(doc)
return DomainExecution(**doc.model_dump(include=set(DomainExecution.__dataclass_fields__)))

async def get_execution(self, execution_id: str) -> DomainExecution | None:
self.logger.info("Searching for execution in MongoDB", execution_id=execution_id)
Expand All @@ -31,7 +32,7 @@ async def get_execution(self, execution_id: str) -> DomainExecution | None:
return None

self.logger.info("Found execution in MongoDB", execution_id=execution_id)
return DomainExecution.model_validate(doc)
return DomainExecution(**doc.model_dump(include=set(DomainExecution.__dataclass_fields__)))

async def write_terminal_result(self, result: ExecutionResultDomain) -> bool:
doc = await ExecutionDocument.find_one(ExecutionDocument.execution_id == result.execution_id)
Expand Down Expand Up @@ -63,7 +64,7 @@ async def get_executions(
]
find_query = find_query.sort(beanie_sort)
docs = await find_query.skip(skip).limit(limit).to_list()
return [DomainExecution.model_validate(doc) for doc in docs]
return [DomainExecution(**doc.model_dump(include=set(DomainExecution.__dataclass_fields__))) for doc in docs]

async def count_executions(self, query: dict[str, Any]) -> int:
return await ExecutionDocument.find(query).count()
Expand Down
35 changes: 25 additions & 10 deletions backend/app/db/repositories/notification_repository.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import dataclasses
from datetime import UTC, datetime, timedelta

import structlog
Expand All @@ -20,9 +21,9 @@ def __init__(self, logger: structlog.stdlib.BoundLogger):
self.logger = logger

async def create_notification(self, create_data: DomainNotificationCreate) -> DomainNotification:
doc = NotificationDocument(**create_data.model_dump())
doc = NotificationDocument(**dataclasses.asdict(create_data))
await doc.insert()
return DomainNotification.model_validate(doc)
return DomainNotification(**doc.model_dump(include=set(DomainNotification.__dataclass_fields__)))

async def update_notification(
self, notification_id: str, user_id: str, update_data: DomainNotificationUpdate
Expand All @@ -33,7 +34,7 @@ async def update_notification(
)
if not doc:
return False
update_dict = update_data.model_dump(exclude_none=True)
update_dict = {k: v for k, v in dataclasses.asdict(update_data).items() if v is not None}
if update_dict:
await doc.set(update_dict)
return True
Expand Down Expand Up @@ -90,7 +91,10 @@ async def list_notifications(
.limit(limit)
.to_list()
)
return [DomainNotification.model_validate(doc) for doc in docs]
return [
DomainNotification(**doc.model_dump(include=set(DomainNotification.__dataclass_fields__)))
for doc in docs
]

async def count_notifications(
self,
Expand Down Expand Up @@ -129,7 +133,10 @@ async def find_due_notifications(self, limit: int = 50) -> list[DomainNotificati
.limit(limit)
.to_list()
)
return [DomainNotification.model_validate(doc) for doc in docs]
return [
DomainNotification(**doc.model_dump(include=set(DomainNotification.__dataclass_fields__)))
for doc in docs
]

async def try_claim_pending(self, notification_id: str) -> bool:
now = datetime.now(UTC)
Expand Down Expand Up @@ -158,7 +165,9 @@ async def get_subscription(
if not doc:
# Default: enabled=True for new users (consistent with get_all_subscriptions)
return DomainNotificationSubscription(user_id=user_id, channel=channel, enabled=True)
return DomainNotificationSubscription.model_validate(doc)
return DomainNotificationSubscription(
**doc.model_dump(include=set(DomainNotificationSubscription.__dataclass_fields__))
)

async def upsert_subscription(
self, user_id: str, channel: NotificationChannel, update_data: DomainSubscriptionUpdate
Expand All @@ -167,20 +176,24 @@ async def upsert_subscription(
NotificationSubscriptionDocument.user_id == user_id,
NotificationSubscriptionDocument.channel == channel,
)
update_dict = update_data.model_dump(exclude_none=True)
update_dict = {k: v for k, v in dataclasses.asdict(update_data).items() if v is not None}
update_dict["updated_at"] = datetime.now(UTC)

if existing:
await existing.set(update_dict)
return DomainNotificationSubscription.model_validate(existing)
return DomainNotificationSubscription(
**existing.model_dump(include=set(DomainNotificationSubscription.__dataclass_fields__))
)
else:
doc = NotificationSubscriptionDocument(
user_id=user_id,
channel=channel,
**update_dict,
)
await doc.insert()
return DomainNotificationSubscription.model_validate(doc)
return DomainNotificationSubscription(
**doc.model_dump(include=set(DomainNotificationSubscription.__dataclass_fields__))
)

async def get_all_subscriptions(self, user_id: str) -> list[DomainNotificationSubscription]:
subs: list[DomainNotificationSubscription] = []
Expand All @@ -190,7 +203,9 @@ async def get_all_subscriptions(self, user_id: str) -> list[DomainNotificationSu
NotificationSubscriptionDocument.channel == channel,
)
if doc:
subs.append(DomainNotificationSubscription.model_validate(doc))
subs.append(DomainNotificationSubscription(
**doc.model_dump(include=set(DomainNotificationSubscription.__dataclass_fields__))
))
else:
subs.append(DomainNotificationSubscription(user_id=user_id, channel=channel, enabled=True))
return subs
Expand Down
12 changes: 8 additions & 4 deletions backend/app/db/repositories/replay_repository.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import dataclasses
from datetime import datetime
from typing import Any, AsyncIterator

Expand All @@ -17,7 +18,7 @@ def __init__(self, logger: structlog.stdlib.BoundLogger) -> None:

async def save_session(self, session: ReplaySessionState) -> None:
existing = await ReplaySessionDocument.find_one(ReplaySessionDocument.session_id == session.session_id)
doc = ReplaySessionDocument(**session.model_dump())
doc = ReplaySessionDocument(**dataclasses.asdict(session))
if existing:
doc.id = existing.id
await doc.save()
Expand All @@ -26,7 +27,7 @@ async def get_session(self, session_id: str) -> ReplaySessionState | None:
doc = await ReplaySessionDocument.find_one(ReplaySessionDocument.session_id == session_id)
if not doc:
return None
return ReplaySessionState.model_validate(doc)
return ReplaySessionState(**doc.model_dump(include=set(ReplaySessionState.__dataclass_fields__)))

async def list_sessions(
self, status: ReplayStatus | None = None, user_id: str | None = None, limit: int = 100, skip: int = 0
Expand All @@ -43,7 +44,10 @@ async def list_sessions(
.limit(limit)
.to_list()
)
return [ReplaySessionState.model_validate(doc) for doc in docs]
return [
ReplaySessionState(**doc.model_dump(include=set(ReplaySessionState.__dataclass_fields__)))
for doc in docs
]

async def update_session_status(self, session_id: str, status: ReplayStatus) -> bool:
doc = await ReplaySessionDocument.find_one(ReplaySessionDocument.session_id == session_id)
Expand All @@ -66,7 +70,7 @@ async def delete_old_sessions(self, cutoff_time: datetime) -> int:
return result.deleted_count if result else 0

async def update_replay_session(self, session_id: str, updates: ReplaySessionUpdate) -> bool:
update_dict = updates.model_dump(exclude_none=True)
update_dict = {k: v for k, v in dataclasses.asdict(updates).items() if v is not None}
if not update_dict:
return False
doc = await ReplaySessionDocument.find_one(ReplaySessionDocument.session_id == session_id)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import dataclasses
from datetime import datetime, timezone
from uuid import uuid4

Expand All @@ -15,10 +16,10 @@ async def count_active(self, language: str) -> int:
async def create_allocation(self, create_data: DomainResourceAllocationCreate) -> DomainResourceAllocation:
doc = ResourceAllocationDocument(
allocation_id=str(uuid4()),
**create_data.model_dump(),
**dataclasses.asdict(create_data),
)
await doc.insert()
return DomainResourceAllocation.model_validate(doc)
return DomainResourceAllocation(**doc.model_dump(include=set(DomainResourceAllocation.__dataclass_fields__)))

async def release_allocation(self, allocation_id: str) -> bool:
doc = await ResourceAllocationDocument.find_one(ResourceAllocationDocument.allocation_id == allocation_id)
Expand Down
Loading
Loading