Files
microdao-daarion/services/calendar-service/storage.py

199 lines
5.5 KiB
Python

"""
Calendar Storage - Database models and operations
"""
from __future__ import annotations
import os
import logging
from datetime import datetime
from typing import Optional, List
from dataclasses import dataclass
logger = logging.getLogger(__name__)
# Simple SQLite-based storage (can be replaced with Postgres)
class CalendarStorage:
"""In-memory/SQLite storage for calendar accounts and reminders"""
def __init__(self, db_session=None):
self.db = db_session
self._accounts = {}
self._reminders = {}
self._idempotency_keys = {}
self._next_id = 1
def create_account(
self,
workspace_id: str,
user_id: str,
provider: str,
username: str,
password: str,
principal_url: str = None,
default_calendar_id: str = None
):
"""Create calendar account"""
# Check if exists
for acc in self._accounts.values():
if (acc.workspace_id == workspace_id and
acc.user_id == user_id and
acc.provider == provider):
# Update
acc.username = username
acc.password = password
acc.principal_url = principal_url
acc.default_calendar_id = default_calendar_id
acc.updated_at = datetime.utcnow()
return acc
# Create new
account = CalendarAccount(
id=f"acc_{self._next_id}",
workspace_id=workspace_id,
user_id=user_id,
provider=provider,
username=username,
password=password, # In production, encrypt this!
principal_url=principal_url,
default_calendar_id=default_calendar_id
)
self._accounts[account.id] = account
self._next_id += 1
logger.info(f"Created calendar account: {account.id}")
return account
def get_account(self, account_id: str) -> Optional[CalendarAccount]:
"""Get account by ID"""
return self._accounts.get(account_id)
def list_accounts(
self,
workspace_id: str,
user_id: str
) -> List[CalendarAccount]:
"""List accounts for user"""
return [
acc for acc in self._accounts.values()
if acc.workspace_id == workspace_id and acc.user_id == user_id
]
def count_accounts(self) -> int:
"""Count total accounts"""
return len(self._accounts)
def create_reminder(
self,
workspace_id: str,
user_id: str,
account_id: str,
event_uid: str,
remind_at: str,
channel: str = "inapp"
) -> "CalendarReminder":
"""Create reminder"""
reminder = CalendarReminder(
id=f"rem_{self._next_id}",
workspace_id=workspace_id,
user_id=user_id,
account_id=account_id,
event_uid=event_uid,
remind_at=datetime.fromisoformat(remind_at),
channel=channel,
status="pending"
)
self._reminders[reminder.id] = reminder
self._next_id += 1
logger.info(f"Created reminder: {reminder.id}")
return reminder
def get_pending_reminders(self) -> List["CalendarReminder"]:
"""Get pending reminders"""
now = datetime.utcnow()
return [
r for r in self._reminders.values()
if r.status == "pending" and r.remind_at <= now
]
def update_reminder_status(
self,
reminder_id: str,
status: str,
error: str = None
):
"""Update reminder status"""
if reminder_id in self._reminders:
self._reminders[reminder_id].status = status
self._reminders[reminder_id].attempts += 1
if error:
self._reminders[reminder_id].last_error = error
def count_pending_reminders(self) -> int:
"""Count pending reminders"""
return len([r for r in self._reminders.values() if r.status == "pending"])
def store_idempotency_key(
self,
key: str,
workspace_id: str,
user_id: str,
event_uid: str
):
"""Store idempotency key"""
self._idempotency_keys[key] = {
"workspace_id": workspace_id,
"user_id": user_id,
"event_uid": event_uid
}
def get_by_idempotency_key(self, key: str) -> Optional[dict]:
"""Get event UID by idempotency key"""
return self._idempotency_keys.get(key)
@dataclass
class CalendarAccount:
"""Calendar account model"""
id: str
workspace_id: str
user_id: str
provider: str
username: str
password: str
principal_url: str = None
default_calendar_id: str = None
created_at: datetime = None
updated_at: datetime = None
def __post_init__(self):
if self.created_at is None:
self.created_at = datetime.utcnow()
if self.updated_at is None:
self.updated_at = datetime.utcnow()
@dataclass
class CalendarReminder:
"""Calendar reminder model"""
id: str
workspace_id: str
user_id: str
account_id: str
event_uid: str
remind_at: datetime
channel: str
status: str = "pending"
attempts: int = 0
last_error: str = None
created_at: datetime = None
def __post_init__(self):
if self.created_at is None:
self.created_at = datetime.utcnow()