Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 58 additions & 9 deletions oura_api_client/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,24 @@ class OuraClient:

BASE_URL = "https://api.ouraring.com/v2"

def __init__(self, access_token: str, retry_config: Optional[RetryConfig] = None):
def __init__(
self,
access_token: str,
retry_config: Optional[RetryConfig] = None,
client_id: Optional[str] = None,
client_secret: Optional[str] = None
):
"""Initialize the Oura client with an access token.

Args:
access_token (str): Your Oura API personal access token
retry_config (RetryConfig, optional): Configuration for retry behavior
client_id (str, optional): Client ID for webhook operations
client_secret (str, optional): Client secret for webhook operations
"""
self.access_token = access_token
self.client_id = client_id
self.client_secret = client_secret
self.headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
Expand Down Expand Up @@ -73,6 +83,8 @@ def _make_request(
params: Optional[Dict[str, Any]] = None,
method: str = "GET",
timeout: Optional[float] = 30.0,
json_data: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, str]] = None,
) -> Dict[str, Any]:
"""Make a request to the Oura API.

Expand All @@ -81,6 +93,8 @@ def _make_request(
params (dict, optional): Query parameters for the request
method (str): HTTP method to use (default: GET)
timeout (float, optional): Request timeout in seconds
json_data (dict, optional): JSON data for request body (POST/PUT/PATCH)
headers (dict, optional): Additional headers to merge with default headers

Returns:
dict: The JSON response from the API
Expand All @@ -100,17 +114,19 @@ def _make_request(

# Wrap the actual request in retry logic if enabled
if self.retry_config.enabled:
return self._make_request_with_retry(url, method, params, timeout, endpoint)
return self._make_request_with_retry(url, method, params, timeout, endpoint, json_data, headers)
else:
return self._make_single_request(url, method, params, timeout, endpoint)
return self._make_single_request(url, method, params, timeout, endpoint, json_data, headers)

def _make_single_request(
self,
url: str,
method: str,
params: Optional[Dict[str, Any]],
timeout: Optional[float],
endpoint: str
endpoint: str,
json_data: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, str]] = None,
) -> Dict[str, Any]:
"""Make a single HTTP request without retry logic.

Expand All @@ -120,6 +136,8 @@ def _make_single_request(
params: Query parameters
timeout: Request timeout
endpoint: Original endpoint for error context
json_data: JSON data for request body
headers: Additional headers to merge with default headers

Returns:
dict: The JSON response from the API
Expand All @@ -128,15 +146,42 @@ def _make_single_request(
OuraAPIError: If the request fails
"""
try:
if method.upper() == "GET":
response = requests.get(url, headers=self.headers, params=params, timeout=timeout)
# Merge headers with default client headers
request_headers = self.headers.copy()
if headers:
request_headers.update(headers)

method_upper = method.upper()
if method_upper == "GET":
response = requests.get(url, headers=request_headers, params=params, timeout=timeout)
elif method_upper == "POST":
if json_data is not None:
response = requests.post(url, headers=request_headers, params=params, json=json_data, timeout=timeout)
else:
response = requests.post(url, headers=request_headers, params=params, timeout=timeout)
elif method_upper == "PUT":
if json_data is not None:
response = requests.put(url, headers=request_headers, params=params, json=json_data, timeout=timeout)
else:
response = requests.put(url, headers=request_headers, params=params, timeout=timeout)
elif method_upper == "PATCH":
if json_data is not None:
response = requests.patch(url, headers=request_headers, params=params, json=json_data, timeout=timeout)
else:
response = requests.patch(url, headers=request_headers, params=params, timeout=timeout)
elif method_upper == "DELETE":
response = requests.delete(url, headers=request_headers, params=params, timeout=timeout)
else:
raise ValueError(f"HTTP method {method} is not supported yet")
raise ValueError(f"HTTP method {method} is not supported")

# Check for HTTP errors
if not response.ok:
raise create_api_error(response, endpoint)

# Handle empty responses (e.g., for DELETE requests)
if response.status_code == 204 or not response.content.strip():
return {}

return response.json()

except requests.exceptions.Timeout as e:
Expand All @@ -152,7 +197,9 @@ def _make_request_with_retry(
method: str,
params: Optional[Dict[str, Any]],
timeout: Optional[float],
endpoint: str
endpoint: str,
json_data: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, str]] = None,
) -> Dict[str, Any]:
"""Make HTTP request with retry logic.

Expand All @@ -162,6 +209,8 @@ def _make_request_with_retry(
params: Query parameters
timeout: Request timeout
endpoint: Original endpoint for error context
json_data: JSON data for request body
headers: Additional headers to merge with default headers

Returns:
dict: The JSON response from the API
Expand All @@ -176,6 +225,6 @@ def _make_request_with_retry(
jitter=self.retry_config.jitter
)
def make_request():
return self._make_single_request(url, method, params, timeout, endpoint)
return self._make_single_request(url, method, params, timeout, endpoint, json_data, headers)

return make_request()
4 changes: 3 additions & 1 deletion oura_api_client/api/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ class Webhook(BaseRouter):
def _get_webhook_headers(self) -> dict:
"""Helper to construct headers for webhook requests."""
if (not hasattr(self.client, 'client_id') or
not hasattr(self.client, 'client_secret')):
not hasattr(self.client, 'client_secret') or
self.client.client_id is None or
self.client.client_secret is None):
# This is a fallback or error case. Ideally, the OuraClient
# should be initialized with client_id and client_secret if
# webhook management is to be used. For now, we'll raise an error
Expand Down
Loading