feat: 增强日历功能并集成 AgentScope 代理服务

This commit is contained in:
qzl
2026-03-11 15:28:29 +08:00
parent e55e445906
commit e20e7d2a02
85 changed files with 5175 additions and 885 deletions
+5 -1
View File
@@ -365,7 +365,11 @@ def _list_auth_users(client: Any) -> list[Any]:
while page <= max_pages:
response = client.auth.admin.list_users(page=page, per_page=100)
batch = list(getattr(response, "users", []))
batch = (
list(response)
if isinstance(response, list)
else list(getattr(response, "users", []))
)
users.extend(batch)
if len(batch) < 100:
+56 -3
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Protocol
from uuid import UUID
@@ -21,11 +22,20 @@ class FriendshipRepository(Protocol):
"""Protocol defining the friendship repository interface."""
async def create_request(
self, initiator_id: UUID, recipient_id: UUID
self, initiator_id: UUID, recipient_id: UUID, content: str | None = None
) -> tuple[Friendship, InboxMessage]:
"""Create a friendship request and inbox message."""
...
async def reactivate_request(
self,
friendship: Friendship,
initiator_id: UUID,
content: str | None = None,
) -> tuple[Friendship, InboxMessage]:
"""Reactivate a declined or canceled friendship request."""
...
async def get_friendship_between_users(
self, user_id_1: UUID, user_id_2: UUID
) -> Friendship | None:
@@ -70,18 +80,21 @@ class SQLAlchemyFriendshipRepository(BaseRepository[Friendship]):
super().__init__(session, Friendship)
async def create_request(
self, initiator_id: UUID, recipient_id: UUID
self, initiator_id: UUID, recipient_id: UUID, content: str | None = None
) -> tuple[Friendship, InboxMessage]:
try:
user_low_id = min(initiator_id, recipient_id)
user_high_id = max(initiator_id, recipient_id)
now = datetime.now(timezone.utc)
friendship = Friendship(
user_low_id=user_low_id,
user_high_id=user_high_id,
initiator_id=initiator_id,
status=FriendshipStatus.PENDING,
requested_at=UUID(int=0),
requested_at=now,
created_by=initiator_id,
updated_by=initiator_id,
)
self._session.add(friendship)
await self._session.flush()
@@ -91,7 +104,9 @@ class SQLAlchemyFriendshipRepository(BaseRepository[Friendship]):
sender_id=initiator_id,
message_type=InboxMessageType.FRIEND_REQUEST,
friendship_id=friendship.id,
content=content,
status=InboxMessageStatus.PENDING,
created_by=initiator_id,
)
self._session.add(inbox)
await self._session.flush()
@@ -105,6 +120,44 @@ class SQLAlchemyFriendshipRepository(BaseRepository[Friendship]):
)
raise
async def reactivate_request(
self,
friendship: Friendship,
initiator_id: UUID,
content: str | None = None,
) -> tuple[Friendship, InboxMessage]:
try:
now = datetime.now(timezone.utc)
friendship.status = FriendshipStatus.PENDING
friendship.requested_at = now
friendship.initiator_id = initiator_id
friendship.updated_by = initiator_id
inbox = InboxMessage(
recipient_id=(
friendship.user_low_id
if initiator_id == friendship.user_high_id
else friendship.user_high_id
),
sender_id=initiator_id,
message_type=InboxMessageType.FRIEND_REQUEST,
friendship_id=friendship.id,
content=content,
status=InboxMessageStatus.PENDING,
created_by=initiator_id,
)
self._session.add(inbox)
await self._session.flush()
return friendship, inbox
except SQLAlchemyError:
logger.exception(
"Failed to reactivate friendship request",
friendship_id=str(friendship.id),
initiator_id=str(initiator_id),
)
raise
async def get_friendship_between_users(
self, user_id_1: UUID, user_id_2: UUID
) -> Friendship | None:
+8 -3
View File
@@ -7,7 +7,6 @@ from fastapi import APIRouter, Depends, status
from v1.friendships.dependencies import get_friendship_service
from v1.friendships.schemas import (
FriendRequestAction,
FriendRequestCreate,
FriendRequestResponse,
FriendResponse,
@@ -44,13 +43,20 @@ async def get_outgoing_requests(
return await service.get_outgoing_requests()
@router.get("/requests/{friendship_id}", response_model=FriendRequestResponse)
async def get_friendship_request(
friendship_id: UUID,
service: Annotated[FriendshipService, Depends(get_friendship_service)],
) -> FriendRequestResponse:
return await service.get_request_by_id(friendship_id)
@router.post(
"/requests/{friendship_id}/accept",
response_model=FriendRequestResponse,
)
async def accept_friend_request(
friendship_id: UUID,
_: FriendRequestAction,
service: Annotated[FriendshipService, Depends(get_friendship_service)],
) -> FriendRequestResponse:
return await service.accept_request(friendship_id)
@@ -62,7 +68,6 @@ async def accept_friend_request(
)
async def decline_friend_request(
friendship_id: UUID,
_: FriendRequestAction,
service: Annotated[FriendshipService, Depends(get_friendship_service)],
) -> FriendRequestResponse:
return await service.decline_request(friendship_id)
+129 -33
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Literal, cast
from uuid import UUID
from fastapi import HTTPException
@@ -10,8 +10,8 @@ from sqlalchemy.exc import SQLAlchemyError
from core.auth.models import CurrentUser
from core.db.base_service import BaseService
from core.logging import get_logger
from models.friendships import FriendshipStatus
from models.inbox_messages import InboxMessageStatus, InboxMessageType
from models.friendships import Friendship, FriendshipStatus
from models.inbox_messages import InboxMessage, InboxMessageStatus, InboxMessageType
from v1.friendships.repository import FriendshipRepository
from v1.friendships.schemas import (
FriendRequestCreate,
@@ -67,18 +67,47 @@ class FriendshipService(BaseService):
user_id, target_user_id
)
if existing:
if existing.status == FriendshipStatus.ACCEPTED:
raise HTTPException(
status_code=400, detail="Already friends with this user"
)
if existing.status == FriendshipStatus.BLOCKED:
raise HTTPException(
status_code=400, detail="Cannot send friend request to blocked user"
)
match existing.status:
case FriendshipStatus.ACCEPTED:
raise HTTPException(
status_code=400, detail="Already friends with this user"
)
case FriendshipStatus.BLOCKED:
raise HTTPException(
status_code=400,
detail="Cannot send friend request to blocked user",
)
case FriendshipStatus.PENDING:
raise HTTPException(
status_code=400, detail="Friend request already sent"
)
case _:
# DECLINED, CANCELED - 允许重新发送
try:
friendship, inbox = await self._repository.reactivate_request(
existing, user_id, request.content
)
await self._session.commit()
except SQLAlchemyError:
await self._session.rollback()
raise HTTPException(
status_code=503, detail="Friendship service unavailable"
)
logger.info(
"friend_request_resent",
extra={
"initiator_id": str(user_id),
"target_id": str(target_user_id),
},
)
return await self._build_friend_request_response(
friendship, inbox, user_id, target_user_id
)
try:
friendship, inbox = await self._repository.create_request(
user_id, target_user_id
user_id, target_user_id, request.content
)
await self._session.commit()
except SQLAlchemyError:
@@ -92,16 +121,8 @@ class FriendshipService(BaseService):
extra={"initiator_id": str(user_id), "target_id": str(target_user_id)},
)
sender = await self._user_repository.get_by_user_id(user_id)
recipient = await self._user_repository.get_by_user_id(target_user_id)
return FriendRequestResponse(
id=friendship.id,
sender=self._build_user_basic_info(sender),
recipient=self._build_user_basic_info(recipient),
content=inbox.content,
status="pending",
created_at=friendship.created_at,
return await self._build_friend_request_response(
friendship, inbox, user_id, target_user_id
)
async def accept_request(self, friendship_id: UUID) -> FriendRequestResponse:
@@ -374,6 +395,61 @@ class FriendshipService(BaseService):
return result
async def get_request_by_id(self, friendship_id: UUID) -> FriendRequestResponse:
user_id = self.require_user_id()
try:
friendship = await self._repository.get_friendship_by_id(friendship_id)
except SQLAlchemyError:
raise HTTPException(
status_code=503, detail="Friendship service unavailable"
)
if friendship is None:
raise HTTPException(status_code=404, detail="Friend request not found")
# Determine sender and recipient based on current user
# initiator_id is the sender
initiator_id = friendship.initiator_id
if initiator_id is None:
raise HTTPException(status_code=400, detail="Invalid friendship data")
if friendship.user_low_id != user_id and friendship.user_high_id != user_id:
raise HTTPException(
status_code=403, detail="Not authorized to view this request"
)
sender = await self._user_repository.get_by_user_id(initiator_id)
recipient_id = (
friendship.user_low_id
if friendship.user_low_id != initiator_id
else friendship.user_high_id
)
recipient = await self._user_repository.get_by_user_id(recipient_id)
# Map FriendshipStatus to response status
status_value: Literal["pending", "accepted", "rejected", "canceled"]
status_map = {
FriendshipStatus.PENDING: "pending",
FriendshipStatus.ACCEPTED: "accepted",
FriendshipStatus.DECLINED: "rejected",
FriendshipStatus.CANCELED: "canceled",
FriendshipStatus.BLOCKED: "canceled",
}
status_value = cast(
Literal["pending", "accepted", "rejected", "canceled"],
status_map.get(friendship.status, "pending"),
)
return FriendRequestResponse(
id=friendship.id,
sender=self._build_user_basic_info(sender),
recipient=self._build_user_basic_info(recipient),
content=None,
status=status_value,
created_at=friendship.created_at,
)
async def get_outgoing_requests(self) -> list[FriendRequestResponse]:
user_id = self.require_user_id()
@@ -386,13 +462,9 @@ class FriendshipService(BaseService):
result: list[FriendRequestResponse] = []
for friendship in outgoing:
recipient_id = (
friendship.user_low_id
if friendship.initiator_id == friendship.user_high_id
else friendship.user_high_id
)
other_user_id = self._get_other_user_id(friendship, user_id)
sender = await self._user_repository.get_by_user_id(user_id)
recipient = await self._user_repository.get_by_user_id(recipient_id)
recipient = await self._user_repository.get_by_user_id(other_user_id)
result.append(
FriendRequestResponse(
@@ -419,11 +491,7 @@ class FriendshipService(BaseService):
result: list[FriendResponse] = []
for friendship in friendships:
friend_id = (
friendship.user_high_id
if friendship.user_low_id == user_id
else friendship.user_low_id
)
friend_id = self._get_other_user_id(friendship, user_id)
friend = await self._user_repository.get_by_user_id(friend_id)
result.append(
@@ -499,3 +567,31 @@ class FriendshipService(BaseService):
username=p.username,
avatar_url=p.avatar_url if hasattr(p, "avatar_url") else None,
)
async def _build_friend_request_response(
self,
friendship: "Friendship",
inbox: "InboxMessage",
initiator_id: UUID,
recipient_id: UUID,
) -> "FriendRequestResponse":
from v1.friendships.schemas import FriendRequestResponse
sender = await self._user_repository.get_by_user_id(initiator_id)
recipient = await self._user_repository.get_by_user_id(recipient_id)
return FriendRequestResponse(
id=friendship.id,
sender=self._build_user_basic_info(sender),
recipient=self._build_user_basic_info(recipient),
content=inbox.content,
status="pending",
created_at=friendship.created_at,
)
def _get_other_user_id(self, friendship: Friendship, current_user_id: UUID) -> UUID:
return (
friendship.user_high_id
if friendship.user_low_id == current_user_id
else friendship.user_low_id
)
+11 -18
View File
@@ -21,13 +21,10 @@ class InboxMessageRepository(Protocol):
self, message_id: UUID, recipient_id: UUID
) -> InboxMessage | None: ...
async def list_by_recipient(
self, recipient_id: UUID, status: str | None = None
self, recipient_id: UUID, is_read: bool | None = None
) -> list[InboxMessage]: ...
async def update_status(
self,
message_id: UUID,
recipient_id: UUID,
status: str,
async def mark_as_read(
self, message_id: UUID, recipient_id: UUID
) -> InboxMessage | None: ...
@@ -67,7 +64,7 @@ class SQLAlchemyInboxMessageRepository:
raise
async def list_by_recipient(
self, recipient_id: UUID, status: str | None = None
self, recipient_id: UUID, is_read: bool | None = None
) -> list[InboxMessage]:
try:
stmt = (
@@ -75,30 +72,27 @@ class SQLAlchemyInboxMessageRepository:
.where(InboxMessage.recipient_id == recipient_id)
.order_by(InboxMessage.created_at.desc())
)
if status is not None:
stmt = stmt.where(InboxMessage.status == status)
if is_read is not None:
stmt = stmt.where(InboxMessage.is_read == is_read)
result = await self._session.execute(stmt)
return list(result.scalars().all())
except SQLAlchemyError:
logger.exception(
"Inbox message list failed",
recipient_id=str(recipient_id),
status=status,
is_read=is_read,
)
raise
async def update_status(
self,
message_id: UUID,
recipient_id: UUID,
status: str,
async def mark_as_read(
self, message_id: UUID, recipient_id: UUID
) -> InboxMessage | None:
try:
stmt = (
update(InboxMessage)
.where(InboxMessage.id == message_id)
.where(InboxMessage.recipient_id == recipient_id)
.values(status=status, is_read=True)
.values(is_read=True)
.returning(InboxMessage)
)
result = await self._session.execute(stmt)
@@ -106,9 +100,8 @@ class SQLAlchemyInboxMessageRepository:
return result.scalar_one_or_none()
except SQLAlchemyError:
logger.exception(
"Inbox message status update failed",
"Inbox message mark as read failed",
message_id=str(message_id),
recipient_id=str(recipient_id),
status=status,
)
raise
+6 -21
View File
@@ -6,12 +6,7 @@ from uuid import UUID
from fastapi import APIRouter, Depends, Query
from v1.inbox_messages.dependencies import get_inbox_message_service
from v1.inbox_messages.schemas import (
InboxMessageAcceptRequest,
InboxMessageListRequest,
InboxMessageResponse,
InboxMessageStatus,
)
from v1.inbox_messages.schemas import InboxMessageResponse
from v1.inbox_messages.service import InboxMessageService
router = APIRouter(prefix="/inbox/messages", tags=["inbox-messages"])
@@ -20,24 +15,14 @@ router = APIRouter(prefix="/inbox/messages", tags=["inbox-messages"])
@router.get("", response_model=list[InboxMessageResponse])
async def list_inbox_messages(
service: Annotated[InboxMessageService, Depends(get_inbox_message_service)],
status: InboxMessageStatus | None = Query(default=None),
is_read: bool | None = Query(default=None, description="Filter by read status"),
) -> list[InboxMessageResponse]:
request = InboxMessageListRequest(status=status)
return await service.list_messages(request)
return await service.list_messages(is_read=is_read)
@router.post("/{message_id}/accept", response_model=InboxMessageResponse)
async def accept_inbox_message(
message_id: UUID,
request: InboxMessageAcceptRequest,
service: Annotated[InboxMessageService, Depends(get_inbox_message_service)],
) -> InboxMessageResponse:
return await service.accept_invitation(message_id, request)
@router.post("/{message_id}/dismiss", response_model=InboxMessageResponse)
async def dismiss_inbox_message(
@router.patch("/{message_id}/read", response_model=InboxMessageResponse)
async def mark_as_read(
message_id: UUID,
service: Annotated[InboxMessageService, Depends(get_inbox_message_service)],
) -> InboxMessageResponse:
return await service.dismiss_invitation(message_id)
return await service.mark_as_read(message_id)
+1 -37
View File
@@ -8,31 +8,6 @@ from uuid import UUID
from pydantic import BaseModel, ConfigDict
class PermissionBits:
VIEW: int = 1 # 001
INVITE: int = 2 # 010
EDIT: int = 4 # 100
@classmethod
def encode(cls, view: bool, edit: bool, invite: bool) -> int:
value = 0
if view:
value |= cls.VIEW
if edit:
value |= cls.EDIT
if invite:
value |= cls.INVITE
return value
@classmethod
def decode(cls, permission: int) -> dict[str, bool]:
return {
"view": bool(permission & cls.VIEW),
"edit": bool(permission & cls.EDIT),
"invite": bool(permission & cls.INVITE),
}
class InboxMessageType(str, Enum):
FRIEND_REQUEST = "friend_request"
CALENDAR = "calendar"
@@ -55,19 +30,8 @@ class InboxMessageResponse(BaseModel):
sender_id: UUID | None = None
message_type: InboxMessageType
schedule_item_id: UUID | None = None
friendship_id: UUID | None = None
content: str | None = None
is_read: bool = False
status: InboxMessageStatus = InboxMessageStatus.PENDING
created_at: datetime
class InboxMessageListRequest(BaseModel):
status: InboxMessageStatus | None = None
class InboxMessageAcceptRequest(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
permission_view: bool = True
permission_edit: bool = False
permission_invite: bool = False
+22 -96
View File
@@ -12,18 +12,11 @@ from core.auth.models import CurrentUser
from core.db.base_service import BaseService
from core.logging import get_logger
from models.inbox_messages import InboxMessage
from models.schedule_subscriptions import (
ScheduleSubscription,
SubscriptionStatus,
)
from v1.inbox_messages.repository import InboxMessageRepository
from v1.inbox_messages.schemas import (
InboxMessageAcceptRequest,
InboxMessageListRequest,
InboxMessageResponse,
InboxMessageStatus,
InboxMessageStatus as SchemaInboxMessageStatus,
InboxMessageType,
PermissionBits,
)
if TYPE_CHECKING:
@@ -47,13 +40,12 @@ class InboxMessageService(BaseService):
self._session = session
async def list_messages(
self, request: InboxMessageListRequest
self, is_read: bool | None = None
) -> list[InboxMessageResponse]:
user_id = self.require_user_id()
try:
status = request.status.value if request.status else None
messages = await self._repository.list_by_recipient(user_id, status)
messages = await self._repository.list_by_recipient(user_id, is_read)
except SQLAlchemyError:
logger.exception("Failed to list inbox messages", user_id=str(user_id))
raise HTTPException(
@@ -62,65 +54,18 @@ class InboxMessageService(BaseService):
return [self._to_response(message) for message in messages]
async def accept_invitation(
self,
message_id: UUID,
request: InboxMessageAcceptRequest,
) -> InboxMessageResponse:
async def mark_as_read(self, message_id: UUID) -> InboxMessageResponse:
user_id = self.require_user_id()
try:
message = await self._repository.get_by_id(message_id, user_id)
if message is None:
raise HTTPException(status_code=404, detail="Inbox message not found")
if message.status.value != InboxMessageStatus.PENDING.value:
raise HTTPException(
status_code=400, detail="Inbox message already handled"
)
if (
message.message_type.value != InboxMessageType.CALENDAR.value
or message.schedule_item_id is None
):
raise HTTPException(
status_code=400, detail="Message is not a calendar invitation"
)
invited_permission = self._parse_invited_permission(message.content)
requested_permission = PermissionBits.encode(
request.permission_view,
request.permission_edit,
request.permission_invite,
)
final_permission = requested_permission & invited_permission
if final_permission == 0:
raise HTTPException(
status_code=400,
detail="No valid permissions requested (must be subset of invited permissions)",
)
subscription = ScheduleSubscription(
item_id=message.schedule_item_id,
subscriber_id=user_id,
permission=final_permission,
status=SubscriptionStatus.ACTIVE,
created_by=user_id,
)
self._session.add(subscription)
updated = await self._repository.update_status(
message_id,
user_id,
InboxMessageStatus.ACCEPTED.value,
)
updated = await self._repository.mark_as_read(message_id, user_id)
if updated is None:
await self._session.rollback()
raise HTTPException(status_code=404, detail="Inbox message not found")
await self._session.commit()
except HTTPException:
raise
except SQLAlchemyError:
await self._session.rollback()
logger.exception(
"Failed to accept inbox invitation",
"Failed to mark inbox message as read",
message_id=str(message_id),
user_id=str(user_id),
)
@@ -130,49 +75,30 @@ class InboxMessageService(BaseService):
return self._to_response(updated)
async def dismiss_invitation(self, message_id: UUID) -> InboxMessageResponse:
user_id = self.require_user_id()
try:
message = await self._repository.get_by_id(message_id, user_id)
if message is None:
raise HTTPException(status_code=404, detail="Inbox message not found")
if message.status.value != InboxMessageStatus.PENDING.value:
raise HTTPException(
status_code=400, detail="Inbox message already handled"
)
updated = await self._repository.update_status(
message_id,
user_id,
InboxMessageStatus.DISMISSED.value,
)
await self._session.commit()
except SQLAlchemyError:
await self._session.rollback()
logger.exception(
"Failed to dismiss inbox invitation",
message_id=str(message_id),
user_id=str(user_id),
)
raise HTTPException(
status_code=503, detail="Inbox message store unavailable"
)
if updated is None:
raise HTTPException(status_code=404, detail="Inbox message not found")
return self._to_response(updated)
def _to_response(self, message: InboxMessage) -> InboxMessageResponse:
status_value = (
message.status.value if hasattr(message.status, "value") else message.status
)
message_type_value = (
message.message_type.value
if hasattr(message.message_type, "value")
else message.message_type
)
return InboxMessageResponse(
id=message.id,
recipient_id=message.recipient_id,
sender_id=message.sender_id,
message_type=InboxMessageType(message.message_type),
message_type=InboxMessageType(message_type_value),
schedule_item_id=message.schedule_item_id,
friendship_id=(
message.friendship_id
if isinstance(message.friendship_id, UUID)
or message.friendship_id is None
else None
),
content=message.content,
is_read=bool(message.is_read),
status=InboxMessageStatus(message.status),
status=SchemaInboxMessageStatus(status_value),
created_at=message.created_at,
)
+3 -5
View File
@@ -9,7 +9,6 @@ from fastapi import APIRouter, Depends, Query
from v1.schedule_items.dependencies import get_schedule_item_service
from v1.schedule_items.schemas import (
ScheduleItemCreateRequest,
ScheduleItemListItem,
ScheduleItemListRequest,
ScheduleItemResponse,
ScheduleItemShareRequest,
@@ -30,15 +29,14 @@ async def create_schedule_item(
return await service.create(request)
@router.get("", response_model=list[ScheduleItemListItem])
@router.get("", response_model=list[ScheduleItemResponse])
async def list_schedule_items(
service: Annotated[ScheduleItemService, Depends(get_schedule_item_service)],
start_at: datetime = Query(..., description="Start date/time for range query"),
end_at: datetime = Query(..., description="End date/time for range query"),
) -> list[ScheduleItemListItem]:
) -> list[ScheduleItemResponse]:
request = ScheduleItemListRequest(start_at=start_at, end_at=end_at)
items = await service.list_by_date_range(request)
return [ScheduleItemListItem.model_validate(item) for item in items]
return await service.list_by_date_range(request)
@router.get("/{item_id}", response_model=ScheduleItemResponse)
+7 -2
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
from datetime import datetime
from enum import Enum
from typing import Literal
from typing import ClassVar
from uuid import UUID
@@ -14,6 +15,8 @@ class AttachmentType(str, Enum):
class ScheduleItemMetadataAttachment(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
name: str
type: AttachmentType
visible_to: list[UUID] = Field(default_factory=list)
@@ -23,11 +26,13 @@ class ScheduleItemMetadataAttachment(BaseModel):
class ScheduleItemMetadata(BaseModel):
color: str | None = None
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
color: str | None = Field(default=None, pattern=r"^#[0-9A-Fa-f]{6}$")
location: str | None = None
notes: str | None = None
attachments: list[ScheduleItemMetadataAttachment] = Field(default_factory=list)
version: int = 1
version: Literal[1] = 1
class ScheduleItemStatus(str, Enum):
+18 -4
View File
@@ -87,7 +87,7 @@ class ScheduleItemService(BaseService):
"start_at": request.start_at,
"end_at": request.end_at,
"timezone": request.timezone,
"metadata": request.metadata.model_dump() if request.metadata else {},
"extra_metadata": request.metadata.model_dump() if request.metadata else {},
"source_type": source_type,
"status": ScheduleItemStatus.ACTIVE,
"created_by": user_id,
@@ -136,7 +136,13 @@ class ScheduleItemService(BaseService):
# Handle metadata separately (model_dump returns dict)
if "metadata" in update_data and update_data["metadata"] is not None:
update_data["metadata"] = update_data["metadata"].model_dump()
metadata_value = update_data["metadata"]
update_data["extra_metadata"] = (
metadata_value.model_dump()
if hasattr(metadata_value, "model_dump")
else metadata_value
)
del update_data["metadata"]
# Validate time range
next_start = update_data.get("start_at", existing.start_at)
@@ -275,6 +281,14 @@ class ScheduleItemService(BaseService):
return ScheduleItemShareResponse(message="Calendar invitation sent")
def _to_response(self, item: ScheduleItem) -> ScheduleItemResponse:
status_value = (
item.status.value if hasattr(item.status, "value") else item.status
)
source_type_value = (
item.source_type.value
if hasattr(item.source_type, "value")
else item.source_type
)
return ScheduleItemResponse(
id=item.id,
title=item.title,
@@ -285,8 +299,8 @@ class ScheduleItemService(BaseService):
metadata=ScheduleItemMetadata.model_validate(item.extra_metadata)
if item.extra_metadata
else None,
status=ScheduleItemStatus(item.status.value),
source_type=ScheduleItemSourceType(item.source_type.value),
status=ScheduleItemStatus(str(status_value)),
source_type=ScheduleItemSourceType(str(source_type_value)),
created_at=item.created_at,
updated_at=item.updated_at,
)
+1
View File
@@ -69,6 +69,7 @@ def get_current_user(authorization: str | None = Header(default=None)) -> Curren
logger.warning(
"JWT validation failed",
error_type=type(exc).__name__,
reason=str(exc),
)
raise HTTPException(status_code=401, detail="Unauthorized") from exc