Skip to main content
Glama
oura_client.py14.1 kB
"""Oura API client with OAuth2 token management.""" import httpx from typing import Optional, Any from oura_mcp.config import config class OuraAPIError(Exception): """Base exception for Oura API errors.""" def __init__(self, status_code: int, message: str, detail: Optional[str] = None): self.status_code = status_code self.message = message self.detail = detail super().__init__( f"[{status_code}] {message}: {detail}" if detail else f"[{status_code}] {message}" ) class OuraClient: """Client for interacting with Oura API v2.""" BASE_URL = "https://api.ouraring.com" def __init__(self, access_token: Optional[str] = None): """ Initialize the Oura API client. Args: access_token: OAuth access token (optional, will use config if not provided) """ self.config = config self._access_token = access_token self._client = httpx.Client(timeout=30.0) def __del__(self): """Cleanup HTTP client.""" if hasattr(self, "_client"): self._client.close() def _get_headers(self) -> dict[str, str]: """Get authorization headers for API requests.""" # Use provided token first, fall back to config token = self._access_token or self.config.access_token if not token: raise OuraAPIError( 401, "No access token available", "Please configure OURA_ACCESS_TOKEN or authenticate via OAuth", ) return { "Authorization": f"Bearer {token}", } def refresh_access_token(self) -> bool: """ Refresh the access token using the refresh token. ⚠️ WARNING: OAuth2 token refresh is NOT TESTED YET! Returns: True if refresh was successful, False otherwise """ if not self.config.refresh_token: return False if not (self.config.client_id and self.config.client_secret): return False try: response = self._client.post( f"{self.BASE_URL}/oauth/token", data={ "grant_type": "refresh_token", "refresh_token": self.config.refresh_token, "client_id": self.config.client_id, "client_secret": self.config.client_secret, }, headers={"Content-Type": "application/x-www-form-urlencoded"}, ) if response.status_code == 200: data = response.json() new_access_token = data.get("access_token") new_refresh_token = data.get("refresh_token") if new_access_token and new_refresh_token: self.config.save_tokens(new_access_token, new_refresh_token) return True return False except Exception: return False def _request( self, method: str, endpoint: str, params: Optional[dict[str, Any]] = None, retry_on_401: bool = True, ) -> dict[str, Any]: """ Make an authenticated request to the Oura API. Args: method: HTTP method (GET, POST, etc.) endpoint: API endpoint path params: Query parameters retry_on_401: Whether to attempt token refresh on 401 error Returns: JSON response data Raises: OuraAPIError: If the API returns an error """ url = f"{self.BASE_URL}{endpoint}" headers = self._get_headers() try: response = self._client.request( method=method, url=url, params=params, headers=headers, ) # Handle 401 errors with token refresh if response.status_code == 401 and retry_on_401: if self.refresh_access_token(): # Retry the request once with new token return self._request(method, endpoint, params, retry_on_401=False) # Handle error responses if response.status_code >= 400: try: error_data = response.json() title = error_data.get("title", "API Error") detail = error_data.get( "detail", error_data.get("error_description") ) raise OuraAPIError(response.status_code, title, detail) except (ValueError, KeyError): raise OuraAPIError( response.status_code, f"HTTP {response.status_code}", response.text or None, ) return response.json() except httpx.RequestError as e: raise OuraAPIError(0, "Request failed", str(e)) def _handle_pagination( self, endpoint: str, params: Optional[dict[str, Any]] = None ) -> list[dict[str, Any]]: """ Handle paginated API responses. Args: endpoint: API endpoint path params: Query parameters Returns: List of all items from paginated response """ params = params or {} all_items = [] while True: data = self._request("GET", endpoint, params) items = data.get("data", []) all_items.extend(items) # Check for next page next_token = data.get("next_token") if not next_token: break params["next_token"] = next_token return all_items # Personal Information def get_personal_info(self) -> dict[str, Any]: """Get user's personal information.""" return self._request("GET", "/v2/usercollection/personal_info") # Daily Summaries def get_daily_sleep( self, start_date: str, end_date: Optional[str] = None ) -> list[dict[str, Any]]: """ Get daily sleep summaries. Args: start_date: Start date in YYYY-MM-DD format end_date: End date in YYYY-MM-DD format (optional) """ params = {"start_date": start_date} if end_date: params["end_date"] = end_date return self._handle_pagination("/v2/usercollection/daily_sleep", params) def get_daily_activity( self, start_date: str, end_date: Optional[str] = None ) -> list[dict[str, Any]]: """ Get daily activity summaries. Args: start_date: Start date in YYYY-MM-DD format end_date: End date in YYYY-MM-DD format (optional) """ params = {"start_date": start_date} if end_date: params["end_date"] = end_date return self._handle_pagination("/v2/usercollection/daily_activity", params) def get_daily_readiness( self, start_date: str, end_date: Optional[str] = None ) -> list[dict[str, Any]]: """ Get daily readiness summaries. Args: start_date: Start date in YYYY-MM-DD format end_date: End date in YYYY-MM-DD format (optional) """ params = {"start_date": start_date} if end_date: params["end_date"] = end_date return self._handle_pagination("/v2/usercollection/daily_readiness", params) def get_daily_stress( self, start_date: str, end_date: Optional[str] = None ) -> list[dict[str, Any]]: """ Get daily stress summaries. Args: start_date: Start date in YYYY-MM-DD format end_date: End date in YYYY-MM-DD format (optional) """ params = {"start_date": start_date} if end_date: params["end_date"] = end_date return self._handle_pagination("/v2/usercollection/daily_stress", params) def get_daily_spo2( self, start_date: str, end_date: Optional[str] = None ) -> list[dict[str, Any]]: """ Get daily SpO2 summaries (Gen 3 ring only). Args: start_date: Start date in YYYY-MM-DD format end_date: End date in YYYY-MM-DD format (optional) """ params = {"start_date": start_date} if end_date: params["end_date"] = end_date return self._handle_pagination("/v2/usercollection/daily_spo2", params) def get_daily_resilience( self, start_date: str, end_date: Optional[str] = None ) -> list[dict[str, Any]]: """ Get daily resilience summaries. Args: start_date: Start date in YYYY-MM-DD format end_date: End date in YYYY-MM-DD format (optional) """ params = {"start_date": start_date} if end_date: params["end_date"] = end_date return self._handle_pagination("/v2/usercollection/daily_resilience", params) def get_daily_cardiovascular_age( self, start_date: str, end_date: Optional[str] = None ) -> list[dict[str, Any]]: """ Get daily cardiovascular age predictions. Args: start_date: Start date in YYYY-MM-DD format end_date: End date in YYYY-MM-DD format (optional) """ params = {"start_date": start_date} if end_date: params["end_date"] = end_date return self._handle_pagination( "/v2/usercollection/daily_cardiovascular_age", params ) # Detailed Sleep Data def get_sleep_periods( self, start_date: str, end_date: Optional[str] = None ) -> list[dict[str, Any]]: """ Get detailed sleep periods. Args: start_date: Start date in YYYY-MM-DD format end_date: End date in YYYY-MM-DD format (optional) """ params = {"start_date": start_date} if end_date: params["end_date"] = end_date return self._handle_pagination("/v2/usercollection/sleep", params) def get_sleep_time( self, start_date: str, end_date: Optional[str] = None ) -> list[dict[str, Any]]: """ Get optimal bedtime recommendations. Args: start_date: Start date in YYYY-MM-DD format end_date: End date in YYYY-MM-DD format (optional) """ params = {"start_date": start_date} if end_date: params["end_date"] = end_date return self._handle_pagination("/v2/usercollection/sleep_time", params) # Activity & Workouts def get_workouts( self, start_date: str, end_date: Optional[str] = None ) -> list[dict[str, Any]]: """ Get workout summaries. Args: start_date: Start date in YYYY-MM-DD format end_date: End date in YYYY-MM-DD format (optional) """ params = {"start_date": start_date} if end_date: params["end_date"] = end_date return self._handle_pagination("/v2/usercollection/workout", params) def get_sessions( self, start_date: str, end_date: Optional[str] = None ) -> list[dict[str, Any]]: """ Get session data (meditation, breathing, etc.). Args: start_date: Start date in YYYY-MM-DD format end_date: End date in YYYY-MM-DD format (optional) """ params = {"start_date": start_date} if end_date: params["end_date"] = end_date return self._handle_pagination("/v2/usercollection/session", params) # Time-Series Data def get_heartrate( self, start_datetime: str, end_datetime: Optional[str] = None ) -> list[dict[str, Any]]: """ Get heart rate time-series data (5-minute intervals). Args: start_datetime: Start datetime in ISO 8601 format end_datetime: End datetime in ISO 8601 format (optional) """ params = {"start_datetime": start_datetime} if end_datetime: params["end_datetime"] = end_datetime return self._handle_pagination("/v2/usercollection/heartrate", params) # User Annotations def get_enhanced_tags( self, start_date: str, end_date: Optional[str] = None ) -> list[dict[str, Any]]: """ Get user-entered enhanced tags. Args: start_date: Start date in YYYY-MM-DD format end_date: End date in YYYY-MM-DD format (optional) """ params = {"start_date": start_date} if end_date: params["end_date"] = end_date return self._handle_pagination("/v2/usercollection/enhanced_tag", params) # Device Information def get_ring_configuration(self) -> list[dict[str, Any]]: """Get ring configuration and device information.""" return self._handle_pagination("/v2/usercollection/ring_configuration") def get_rest_mode_periods( self, start_date: str, end_date: Optional[str] = None ) -> list[dict[str, Any]]: """ Get rest mode periods. Args: start_date: Start date in YYYY-MM-DD format end_date: End date in YYYY-MM-DD format (optional) """ params = {"start_date": start_date} if end_date: params["end_date"] = end_date return self._handle_pagination("/v2/usercollection/rest_mode_period", params) # Fitness Metrics def get_vo2_max( self, start_date: str, end_date: Optional[str] = None ) -> list[dict[str, Any]]: """ Get VO2 max estimates. Args: start_date: Start date in YYYY-MM-DD format end_date: End date in YYYY-MM-DD format (optional) """ params = {"start_date": start_date} if end_date: params["end_date"] = end_date return self._handle_pagination("/v2/usercollection/vO2_max", params)

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/pokidyshev/oura-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server