techprotrade's picture
Deploy ATOM FastAPI command center runtime (part 4)
4b9d59b verified
Raw
History Blame Contribute Delete
15.6 kB
"""
Calendly Integration Adapter
Provides OAuth-based integration with Calendly for scheduling and meeting management.
"""
import logging
import os
import httpx
from typing import Dict, Any, List, Optional
from datetime import datetime, timedelta
from urllib.parse import urlencode
logger = logging.getLogger(__name__)
class CalendlyAdapter:
"""
Adapter for Calendly OAuth integration.
Supports:
- OAuth 2.0 authentication
- Event type and scheduling management
- Event and invitee operations
- Webhook and user management
"""
def __init__(self, db, workspace_id: str):
self.db = db
self.workspace_id = workspace_id
self.service_name = "calendly"
self.base_url = "https://api.calendly.com"
# OAuth credentials from environment
self.client_id = os.getenv("CALENDLY_CLIENT_ID")
self.client_secret = os.getenv("CALENDLY_CLIENT_SECRET")
self.redirect_uri = os.getenv("CALENDLY_REDIRECT_URI")
# Token storage
self._access_token: Optional[str] = None
_refresh_token: Optional[str] = None
self._token_expires_at: Optional[datetime] = None
self._user_uri: Optional[str] = None
async def get_oauth_url(self) -> str:
"""
Generate Calendly OAuth authorization URL.
Returns:
Authorization URL to redirect user to Calendly OAuth consent screen
"""
if not self.client_id:
raise ValueError("CALENDLY_CLIENT_ID not configured")
# Calendly OAuth endpoint
auth_url = "https://auth.calendly.com/oauth/authorize"
# Build authorization URL
params = {
"client_id": self.client_id,
"redirect_uri": self.redirect_uri,
"response_type": "code",
"scope": "default",
"state": self.workspace_id, # Use workspace_id as state
}
auth_url_with_params = f"{auth_url}?{urlencode(params)}"
logger.info(f"Generated Calendly OAuth URL for workspace {self.workspace_id}")
return auth_url_with_params
async def exchange_code_for_token(self, code: str) -> Dict[str, Any]:
"""
Exchange OAuth authorization code for access token.
Args:
code: Authorization code from OAuth callback
Returns:
Token response with access_token, refresh_token, expires_in, etc.
"""
if not self.client_id or not self.client_secret:
raise ValueError("Calendly OAuth credentials not configured")
token_url = "https://auth.calendly.com/oauth/token"
data = {
"grant_type": "authorization_code",
"code": code,
"redirect_uri": self.redirect_uri,
"client_id": self.client_id,
"client_secret": self.client_secret,
}
try:
async with httpx.AsyncClient() as client:
response = await client.post(token_url, json=data)
response.raise_for_status()
token_data = response.json()
# Store tokens
self._access_token = token_data.get("access_token")
_refresh_token = token_data.get("refresh_token")
# Calculate token expiration (2 hours)
if "expires_in" in token_data:
self._token_expires_at = datetime.now() + timedelta(
seconds=token_data["expires_in"]
)
# Get user URI
await self._get_current_user()
logger.info(f"Successfully obtained Calendly access token for workspace {self.workspace_id}")
return token_data
except httpx.HTTPStatusError as e:
logger.error(f"Calendly token exchange failed: {e}")
raise
async def _get_current_user(self) -> Optional[str]:
"""Get the current user's URI."""
if not self._access_token:
return None
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.base_url}/users/me",
headers={
"Authorization": f"Bearer {self._access_token}"
}
)
response.raise_for_status()
data = response.json()
self._user_uri = data.get("resource", {}).get("uri")
return self._user_uri
except Exception as e:
logger.error(f"Failed to get Calendly user: {e}")
return None
async def test_connection(self) -> bool:
"""
Test the Calendly API connection.
Returns:
True if connection successful, False otherwise
"""
if not self._access_token:
return False
try:
user_uri = await self._get_current_user()
return user_uri is not None
except Exception as e:
logger.error(f"Calendly connection test failed: {e}")
return False
async def get_event_types(self, user_uri: str = None) -> List[Dict[str, Any]]:
"""
Retrieve Calendly event types.
Args:
user_uri: User URI (empty for current user)
Returns:
List of event type objects
"""
if not self._access_token:
raise ValueError("Calendly access token not available")
try:
params = {}
if user_uri:
params["user"] = user_uri
elif self._user_uri:
params["user"] = self._user_uri
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.base_url}/event_types",
headers={
"Authorization": f"Bearer {self._access_token}"
},
params=params
)
response.raise_for_status()
data = response.json()
event_types = data.get("collection", [])
logger.info(f"Retrieved {len(event_types)} Calendly event types for workspace {self.workspace_id}")
return event_types
except Exception as e:
logger.error(f"Failed to retrieve Calendly event types: {e}")
raise
async def get_scheduled_events(self, user_uri: str = None, status: str = "active",
start_date: str = None, end_date: str = None,
limit: int = 20) -> List[Dict[str, Any]]:
"""
Retrieve scheduled Calendly events.
Args:
user_uri: User URI (empty for current user)
status: Event status ("active", "canceled")
start_date: Start date filter (ISO 8601)
end_date: End date filter (ISO 8601)
limit: Maximum number of results
Returns:
List of event objects
"""
if not self._access_token:
raise ValueError("Calendly access token not available")
try:
params = {
"status": status,
"max_results": limit
}
if user_uri or self._user_uri:
params["user"] = user_uri or self._user_uri
if start_date:
params["min_start_time"] = start_date
if end_date:
params["max_start_time"] = end_date
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.base_url}/scheduled_events",
headers={
"Authorization": f"Bearer {self._access_token}"
},
params=params
)
response.raise_for_status()
data = response.json()
events = data.get("collection", [])
logger.info(f"Retrieved {len(events)} Calendly events for workspace {self.workspace_id}")
return events
except Exception as e:
logger.error(f"Failed to retrieve Calendly events: {e}")
raise
async def get_event(self, event_uuid: str) -> Dict[str, Any]:
"""
Retrieve a specific Calendly event by UUID.
Args:
event_uuid: Event UUID
Returns:
Event details with all invitees
"""
if not self._access_token:
raise ValueError("Calendly access token not available")
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.base_url}/scheduled_events/{event_uuid}",
headers={
"Authorization": f"Bearer {self._access_token}"
}
)
response.raise_for_status()
event = response.json()
logger.info(f"Retrieved Calendly event {event_uuid} for workspace {self.workspace_id}")
return event
except Exception as e:
logger.error(f"Failed to retrieve Calendly event {event_uuid}: {e}")
raise
async def get_event_invitees(self, event_uuid: str) -> List[Dict[str, Any]]:
"""
Retrieve all invitees for a Calendly event.
Args:
event_uuid: Event UUID
Returns:
List of invitee objects
"""
if not self._access_token:
raise ValueError("Calendly access token not available")
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.base_url}/scheduled_events/{event_uuid}/invitees",
headers={
"Authorization": f"Bearer {self._access_token}"
}
)
response.raise_for_status()
data = response.json()
invitees = data.get("collection", [])
logger.info(f"Retrieved {len(invitees)} invitees for event {event_uuid}")
return invitees
except Exception as e:
logger.error(f"Failed to retrieve Calendly event invitees: {e}")
raise
async def cancel_event(self, event_uuid: str, cancel_reason: str = None) -> Dict[str, Any]:
"""
Cancel a scheduled Calendly event.
Args:
event_uuid: Event UUID to cancel
cancel_reason: Reason for cancellation
Returns:
Canceled event object
"""
if not self._access_token:
raise ValueError("Calendly access token not available")
try:
data = {"status": "canceled"}
if cancel_reason:
data["cancel_reason"] = cancel_reason
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.base_url}/scheduled_events/{event_uuid}/cancellation",
headers={
"Authorization": f"Bearer {self._access_token}",
"Content-Type": "application/json"
},
json=data
)
response.raise_for_status()
event = response.json()
logger.info(f"Canceled Calendly event {event_uuid} for workspace {self.workspace_id}")
return event
except Exception as e:
logger.error(f"Failed to cancel Calendly event {event_uuid}: {e}")
raise
async def get_webhooks(self, user_uri: str = None) -> List[Dict[str, Any]]:
"""
Retrieve all Calendly webhooks.
Args:
user_uri: User URI (empty for current user)
Returns:
List of webhook subscriptions
"""
if not self._access_token:
raise ValueError("Calendly access token not available")
try:
params = {}
if user_uri or self._user_uri:
params["user"] = user_uri or self._user_uri
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.base_url}/webhook_subscriptions",
headers={
"Authorization": f"Bearer {self._access_token}"
},
params=params
)
response.raise_for_status()
data = response.json()
webhooks = data.get("collection", [])
logger.info(f"Retrieved {len(webhooks)} Calendly webhooks for workspace {self.workspace_id}")
return webhooks
except Exception as e:
logger.error(f"Failed to retrieve Calendly webhooks: {e}")
raise
async def create_webhook(self, url: str, events: List[str],
organization_uri: str = None, user_uri: str = None) -> Dict[str, Any]:
"""
Create a Calendly webhook subscription.
Args:
url: Webhook callback URL
events: List of events to subscribe to
organization_uri: Organization URI
user_uri: User URI
Returns:
Created webhook subscription
"""
if not self._access_token:
raise ValueError("Calendly access token not available")
try:
data = {
"url": url,
"events": events,
"organization": organization_uri,
"scope": "organization" if organization_uri else "user"
}
if user_uri or self._user_uri:
data["user"] = user_uri or self._user_uri
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.base_url}/webhook_subscriptions",
headers={
"Authorization": f"Bearer {self._access_token}",
"Content-Type": "application/json"
},
json=data
)
response.raise_for_status()
webhook = response.json()
logger.info(f"Created Calendly webhook for workspace {self.workspace_id}")
return webhook
except Exception as e:
logger.error(f"Failed to create Calendly webhook: {e}")
raise
async def delete_webhook(self, webhook_uuid: str) -> bool:
"""
Delete a Calendly webhook subscription.
Args:
webhook_uuid: Webhook UUID to delete
Returns:
True if successful
"""
if not self._access_token:
raise ValueError("Calendly access token not available")
try:
async with httpx.AsyncClient() as client:
response = await client.delete(
f"{self.base_url}/webhook_subscriptions/{webhook_uuid}",
headers={
"Authorization": f"Bearer {self._access_token}"
}
)
response.raise_for_status()
logger.info(f"Deleted Calendly webhook {webhook_uuid} in workspace {self.workspace_id}")
return True
except Exception as e:
logger.error(f"Failed to delete Calendly webhook {webhook_uuid}: {e}")
return False