system-prompts-and-models-o.../dealix/integrations/calendar.py
2026-05-01 14:03:52 +03:00

158 lines
5.4 KiB
Python

"""
Calendar integrations — Google Calendar (direct create) and Calendly (link + webhook).
تكاملات التقويم.
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from typing import Any
import httpx
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential
from core.config.settings import get_settings
from core.errors import IntegrationError
from core.logging import get_logger
logger = get_logger(__name__)
@dataclass
class CalendarEventResult:
success: bool
provider: str
event_id: str | None = None
html_link: str | None = None
meeting_link: str | None = None
error: str | None = None
class GoogleCalendarClient:
"""
Lightweight Google Calendar client using a service-account JSON key file.
Uses OAuth2 service account with domain-wide delegation recommended for
production; for personal accounts, use user OAuth flow.
"""
def __init__(self) -> None:
self.settings = get_settings()
@property
def configured(self) -> bool:
return bool(self.settings.google_calendar_credentials_file)
async def create_event(
self,
*,
summary: str,
description: str,
start: datetime,
end: datetime,
attendee_emails: list[str] | None = None,
calendar_id: str | None = None,
) -> CalendarEventResult:
"""Create a Google Calendar event (uses google-api-python-client)."""
if not self.configured:
return CalendarEventResult(
success=False, provider="google", error="Credentials not configured"
)
try:
# Lazy imports so missing deps don't break the import
from google.oauth2 import service_account # type: ignore
from googleapiclient.discovery import build # type: ignore
credentials = service_account.Credentials.from_service_account_file(
self.settings.google_calendar_credentials_file,
scopes=["https://www.googleapis.com/auth/calendar"],
)
service = build("calendar", "v3", credentials=credentials, cache_discovery=False)
event_body: dict[str, Any] = {
"summary": summary,
"description": description,
"start": {
"dateTime": start.isoformat(),
"timeZone": self.settings.app_timezone,
},
"end": {
"dateTime": end.isoformat(),
"timeZone": self.settings.app_timezone,
},
}
if attendee_emails:
event_body["attendees"] = [{"email": e} for e in attendee_emails]
cal_id = calendar_id or self.settings.google_calendar_id
event = (
service.events()
.insert(calendarId=cal_id, body=event_body, sendUpdates="all")
.execute()
)
logger.info("gcal_event_created", event_id=event.get("id"))
return CalendarEventResult(
success=True,
provider="google",
event_id=event.get("id"),
html_link=event.get("htmlLink"),
meeting_link=event.get("hangoutLink"),
)
except Exception as e:
logger.exception("gcal_event_failed", error=str(e))
return CalendarEventResult(success=False, provider="google", error=str(e))
class CalendlyClient:
"""
Calendly client — uses token to fetch scheduled events, return scheduling link.
"""
BASE_URL = "https://api.calendly.com"
def __init__(self) -> None:
self.settings = get_settings()
@property
def configured(self) -> bool:
return self.settings.calendly_api_token is not None
def _headers(self) -> dict[str, str]:
if not self.settings.calendly_api_token:
raise IntegrationError("CALENDLY_API_TOKEN not configured")
token = self.settings.calendly_api_token.get_secret_value()
return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
def scheduling_link(self) -> str | None:
"""Return the public scheduling link (from settings)."""
if not self.settings.calendly_user_uri:
return None
uri = self.settings.calendly_user_uri
if uri.startswith("http"):
return uri
return f"https://calendly.com/{uri}"
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=10),
retry=retry_if_exception_type((httpx.TimeoutException, httpx.HTTPStatusError)),
reraise=True,
)
async def list_scheduled_events(self, count: int = 20) -> list[dict[str, Any]]:
"""Fetch recent scheduled events."""
if not self.configured:
return []
user_uri = self.settings.calendly_user_uri or ""
if not user_uri.startswith("https://"):
# Need full URI for API call
user_uri = f"https://api.calendly.com/users/{user_uri}"
url = f"{self.BASE_URL}/scheduled_events"
params = {"user": user_uri, "count": count}
async with httpx.AsyncClient(timeout=30) as client:
response = await client.get(url, headers=self._headers(), params=params)
response.raise_for_status()
return response.json().get("collection", [])