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
4 changes: 2 additions & 2 deletions oura_api_client/api/vo2_max.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def get_vo2_max_documents(
"next_token": next_token if next_token else None,
}
params = {k: v for k, v in params.items() if v is not None}
response = self.client._make_request("/v2/usercollection/vo2_max", params=params)
response = self.client._make_request("/v2/usercollection/vO2_max", params=params)
return Vo2MaxResponse(**response)

def get_vo2_max_document(self, document_id: str) -> Vo2MaxModel:
Expand All @@ -44,5 +44,5 @@ def get_vo2_max_document(self, document_id: str) -> Vo2MaxModel:
Returns:
Vo2MaxModel: Response containing VO2 max data.
"""
response = self.client._make_request(f"/v2/usercollection/vo2_max/{document_id}")
response = self.client._make_request(f"/v2/usercollection/vO2_max/{document_id}")
return Vo2MaxModel(**response)
162 changes: 102 additions & 60 deletions oura_api_client/api/webhook.py
Original file line number Diff line number Diff line change
@@ -1,111 +1,153 @@
from typing import Optional, List # Added List
from typing import Optional, List
from oura_api_client.api.base import BaseRouter
from oura_api_client.models.webhook import (
WebhookSubscriptionModel,
WebhookListResponse,
# WebhookListResponse, # Removed as API returns List directly
WebhookSubscriptionCreateRequest,
WebhookSubscriptionUpdateRequest
WebhookSubscriptionUpdateRequest,
WebhookOperation,
ExtApiV2DataType
)

class Webhook(BaseRouter):
def list_webhook_subscriptions(self) -> WebhookListResponse:
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'):
# 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 or return base headers,
# but a production client would need proper handling.
raise ValueError("client_id and client_secret must be set in OuraClient for webhook operations.")

# Merge with existing base headers (like Content-Type if needed, or other default headers)
# For this specific API, it seems only x-client-id and x-client-secret are custom.
# The base _make_request should handle common headers like Authorization if that was the case,
# but webhook auth is different.
headers = {
"x-client-id": self.client.client_id,
"x-client-secret": self.client.client_secret,
}
# Add other necessary headers like Content-Type for POST/PUT if not handled by _make_request
# when json_data is present. Typically, requests library does this automatically.
return headers

def list_webhook_subscriptions(self) -> List[WebhookSubscriptionModel]:
"""
List all existing webhook subscriptions.
Note: Oura API v2 for webhooks does not use pagination (next_token).

Returns:
WebhookListResponse: Response containing a list of webhook subscriptions.
API Path: GET /v2/webhook/subscription
"""
response = self.client._make_request("/v2/usercollection/webhook")
return WebhookListResponse(**response)
headers = self._get_webhook_headers()
response_data = self.client._make_request(
"/v2/webhook/subscription",
headers=headers
)
# API returns a list of subscriptions directly
return [WebhookSubscriptionModel(**item) for item in response_data]

def create_webhook_subscription(
self,
callback_url: str,
event_types: List[str],
verification_token: Optional[str] = None,
event_type: WebhookOperation,
data_type: ExtApiV2DataType,
verification_token: str, # Made non-optional as per updated model reflecting spec
) -> WebhookSubscriptionModel:
"""
Create a new webhook subscription.

Args:
callback_url: The URL where webhook notifications will be sent.
event_types: A list of event types to subscribe to.
verification_token: An optional token to verify the callback URL.

Returns:
WebhookSubscriptionModel: The created webhook subscription details.
API Path: POST /v2/webhook/subscription
"""
headers = self._get_webhook_headers()
# Ensure Content-Type is set for POST with JSON body, if not handled by _make_request
if 'Content-Type' not in headers:
headers['Content-Type'] = 'application/json'

request_body = WebhookSubscriptionCreateRequest(
callback_url=callback_url,
event_types=event_types,
event_type=event_type,
data_type=data_type,
verification_token=verification_token,
)
# Pydantic's model_dump(by_alias=True) ensures correct field names are used in the JSON
response = self.client._make_request(
"/v2/usercollection/webhook",
response_data = self.client._make_request(
"/v2/webhook/subscription",
method="POST",
json_data=request_body.model_dump(by_alias=True, exclude_none=True)
json_data=request_body.model_dump(by_alias=True), # exclude_none=True is default for model_dump
headers=headers
)
return WebhookSubscriptionModel(**response)
return WebhookSubscriptionModel(**response_data)

def get_webhook_subscription(self, subscription_id: str) -> WebhookSubscriptionModel:
"""
Get details for a specific webhook subscription.

Args:
subscription_id: The ID of the webhook subscription.

Returns:
WebhookSubscriptionModel: Details of the webhook subscription.
API Path: GET /v2/webhook/subscription/{subscription_id}
"""
response = self.client._make_request(f"/v2/usercollection/webhook/{subscription_id}")
return WebhookSubscriptionModel(**response)
headers = self._get_webhook_headers()
response_data = self.client._make_request(
f"/v2/webhook/subscription/{subscription_id}",
headers=headers
)
return WebhookSubscriptionModel(**response_data)

def update_webhook_subscription(
self,
subscription_id: str,
verification_token: str, # Required
callback_url: Optional[str] = None,
event_types: Optional[List[str]] = None,
verification_token: Optional[str] = None,
event_type: Optional[WebhookOperation] = None,
data_type: Optional[ExtApiV2DataType] = None,
) -> WebhookSubscriptionModel:
"""
Update an existing webhook subscription.

Args:
subscription_id: The ID of the webhook subscription to update.
callback_url: The new callback URL.
event_types: The new list of event types.
verification_token: The new verification token.

Returns:
WebhookSubscriptionModel: The updated webhook subscription details.
API Path: PUT /v2/webhook/subscription/{subscription_id}
"""
headers = self._get_webhook_headers()
if 'Content-Type' not in headers:
headers['Content-Type'] = 'application/json'

request_body = WebhookSubscriptionUpdateRequest(
verification_token=verification_token, # Now required
callback_url=callback_url,
event_types=event_types,
verification_token=verification_token,
event_type=event_type,
data_type=data_type,
)
# Pydantic's model_dump(by_alias=True, exclude_none=True) ensures correct field names and omits unset optionals
response = self.client._make_request(
f"/v2/usercollection/webhook/{subscription_id}",
response_data = self.client._make_request(
f"/v2/webhook/subscription/{subscription_id}",
method="PUT",
json_data=request_body.model_dump(by_alias=True, exclude_none=True)
json_data=request_body.model_dump(by_alias=True, exclude_none=True),
headers=headers
)
return WebhookSubscriptionModel(**response)
return WebhookSubscriptionModel(**response_data)

def delete_webhook_subscription(self, subscription_id: str) -> None:
"""
Delete a webhook subscription.

Args:
subscription_id: The ID of the webhook subscription to delete.

Returns:
None. The API returns a 204 No Content on success.
API Path: DELETE /v2/webhook/subscription/{subscription_id}
"""
headers = self._get_webhook_headers()
self.client._make_request(
f"/v2/usercollection/webhook/{subscription_id}",
method="DELETE"
f"/v2/webhook/subscription/{subscription_id}",
method="DELETE",
headers=headers
)
return None

def renew_webhook_subscription(self, subscription_id: str) -> WebhookSubscriptionModel:
"""
Renew an existing webhook subscription.
API Path: PUT /v2/webhook/subscription/renew/{subscription_id}
"""
headers = self._get_webhook_headers()
# Some APIs might require Content-Type even for PUTs without a body,
# but typically not. If it's needed, _make_request or requests lib handles it,
# or it can be added here.
# if 'Content-Type' not in headers:
# headers['Content-Type'] = 'application/json'

response_data = self.client._make_request(
f"/v2/webhook/subscription/renew/{subscription_id}",
method="PUT",
headers=headers
# No json_data for this specific renew endpoint as per typical renew patterns,
# unless the spec implies a body, which it does not for this path.
)
return WebhookSubscriptionModel(**response_data)

[end of oura_api_client/api/webhook.py]
77 changes: 48 additions & 29 deletions oura_api_client/models/webhook.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,60 @@
from pydantic import BaseModel, Field
from typing import List, Optional
from datetime import datetime
from enum import Enum

class WebhookEventModel(BaseModel): # New model for events within a subscription
event_type: str = Field(alias="event_type") # e.g., "oura_webhook_test.test_event" or specific data types
# Additional fields for an event could be 'timestamp', 'user_id', 'data_id' if provided by API
# For now, keeping it simple as per typical webhook event structures.
class WebhookOperation(str, Enum):
CREATE = "create"
UPDATE = "update"
DELETE = "delete"

class WebhookSubscriptionModel(BaseModel):
id: str # Webhook subscription ID
created_at: datetime = Field(alias="created_at")
updated_at: datetime = Field(None, alias="updated_at") # Optional, as it might not be updated
verification_token: Optional[str] = Field(None, alias="verification_token") # Only present on creation/update
callback_url: str = Field(alias="callback_url")
subscribed_events: Optional[List[WebhookEventModel]] = Field(None, alias="subscribed_events")
# The OpenAPI spec indicates 'event_types' as a list of strings for creation,
# but a successful response for GET might detail them as objects or just list strings.
# Using WebhookEventModel for subscribed_events if the API returns more detail than just strings.
# If it's just strings, this would be:
# subscribed_events: Optional[List[str]] = Field(None, alias="subscribed_events")
# For now, assuming a list of simple event type strings as per common webhook patterns for listing subscriptions.
# Re-adjusting based on typical GET response: usually lists event type strings.
event_types: Optional[List[str]] = Field(None, alias="event_types")
class ExtApiV2DataType(str, Enum):
TAG = "tag"
ENHANCED_TAG = "enhanced_tag"
WORKOUT = "workout"
SESSION = "session"
SLEEP = "sleep"
DAILY_SLEEP = "daily_sleep"
DAILY_READINESS = "daily_readiness"
DAILY_ACTIVITY = "daily_activity"
DAILY_SPO2 = "daily_spo2"
SLEEP_TIME = "sleep_time"
REST_MODE_PERIOD = "rest_mode_period"
RING_CONFIGURATION = "ring_configuration"
DAILY_STRESS = "daily_stress"
DAILY_CARDIOVASCULAR_AGE = "daily_cardiovascular_age"
DAILY_RESILIENCE = "daily_resilience"
VO2_MAX = "vo2_max"
# Note: The OpenAPI spec does not list "heartrate" under ExtApiV2DataType for webhooks,
# but it is a general data type. If webhooks support it, it should be added.
# For now, sticking to the types explicitly listed under Webhook components.

class WebhookSubscriptionModel(BaseModel):
id: str = Field(..., description="Webhook subscription ID")
callback_url: str = Field(..., alias="callback_url")
event_type: WebhookOperation = Field(..., alias="event_type")
data_type: ExtApiV2DataType = Field(..., alias="data_type")
# Assuming created_at and updated_at are not part of the GET response based on spec example for WebhookSubscriptionModel
# If they are, they should be added back. The spec for WebhookSubscriptionModel shows:
# id, callback_url, event_type, data_type, expiration_time
expiration_time: datetime = Field(..., alias="expiration_time")
# verification_token is not part of the response for GET /subscription or GET /subscription/{id}

class WebhookSubscriptionCreateRequest(BaseModel): # For POST request body
callback_url: str = Field(alias="callback_url")
verification_token: Optional[str] = Field(None, alias="verification_token")
event_types: List[str] = Field(alias="event_types")
callback_url: str = Field(..., alias="callback_url")
verification_token: str = Field(..., alias="verification_token") # Made required as per spec
event_type: WebhookOperation = Field(..., alias="event_type")
data_type: ExtApiV2DataType = Field(..., alias="data_type")

class WebhookSubscriptionUpdateRequest(BaseModel): # For PUT request body
verification_token: str = Field(..., alias="verification_token") # Required
callback_url: Optional[str] = Field(None, alias="callback_url")
verification_token: Optional[str] = Field(None, alias="verification_token")
event_types: Optional[List[str]] = Field(None, alias="event_types")
event_type: Optional[WebhookOperation] = Field(None, alias="event_type")
data_type: Optional[ExtApiV2DataType] = Field(None, alias="data_type")

# No longer using WebhookListResponse as the API returns a direct list.
# class WebhookListResponse(BaseModel):
# data: List[WebhookSubscriptionModel]

# Response for listing multiple webhooks
class WebhookListResponse(BaseModel):
data: List[WebhookSubscriptionModel]
# Oura's list webhooks endpoint does not use next_token based on v2 spec
# next_token: Optional[str] = None
# Model for the renew response (same as WebhookSubscriptionModel)
WebhookRenewResponse = WebhookSubscriptionModel
1 change: 1 addition & 0 deletions parse_openapi.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import logging

def parse_openapi_spec(spec_content):
"""
Expand Down
Loading
Loading