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
32 changes: 30 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,43 @@ The client automatically retries on:
### Each endpoint supports:
- `get_xxx_documents()` - Get multiple documents with pagination
- `get_xxx_document(document_id)` - Get a single document by ID
- `stream()` - **NEW!** Stream all documents automatically handling pagination
- Date filtering with `start_date` and `end_date` (accepts strings or date objects)
- Pagination with `next_token`

## Advanced Usage

### Pagination
### Seamless Pagination with Stream Methods

The easiest way to work with paginated data is using the `.stream()` methods, which automatically handle pagination and yield individual items:

```python
# Stream all sleep data seamlessly - no manual pagination needed!
for sleep_record in client.daily_sleep.stream(start_date="2024-01-01"):
print(f"Date: {sleep_record.day}, Score: {sleep_record.score}")

# Stream heart rate data for analysis
for hr_sample in client.heartrate.stream(start_date="2024-12-20"):
print(f"Heart rate: {hr_sample.bpm} at {hr_sample.timestamp}")

# Stream activity data with date range
for activity in client.daily_activity.stream(
start_date="2024-01-01",
end_date="2024-01-31"
):
print(f"Steps: {activity.steps}, Calories: {activity.active_calories}")

# Stream session data
for session in client.session.stream(start_date="2024-01-01"):
print(f"Session: {session.type} from {session.start_datetime}")
```

### Manual Pagination (Advanced)

For more control over pagination, you can still handle it manually:

```python
# Iterate through all sleep data using pagination
# Iterate through all sleep data using manual pagination
next_token = None
all_sleep_data = []

Expand Down
81 changes: 81 additions & 0 deletions demo_pagination.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""
Demonstration of the new pagination helpers in the Oura API client.

This script shows how to use the new .stream() methods for seamless pagination.
"""

import os
from datetime import date, timedelta
from oura_api_client.api.client import OuraClient


def demo_pagination_helpers():
"""Demonstrate the pagination helpers functionality."""

print("πŸ”„ Oura API Pagination Helpers Demo")
print("=" * 50)

# Note: This demo shows the API usage but won't make real calls
# without a valid access token and data

print("\n1. Setting up client...")
# Use a dummy token for demo (replace with real token for actual use)
client = OuraClient(access_token="demo_token_here")

print("\n2. NEW: Seamless pagination with .stream() methods")
print("-" * 50)

# Calculate date range for last 7 days
end_date = date.today()
start_date = end_date - timedelta(days=7)

print(f"\nπŸ›Œ Streaming sleep data from {start_date} to {end_date}:")
print("for sleep_record in client.daily_sleep.stream(start_date='2024-01-01'):")
print(" print(f'Date: {sleep_record.day}, Score: {sleep_record.score}')")

print(f"\nπŸ’“ Streaming heart rate data:")
print("for hr_sample in client.heartrate.stream(start_date='2024-01-01'):")
print(" print(f'HR: {hr_sample.bpm} at {hr_sample.timestamp}')")

print(f"\nπŸƒ Streaming activity data:")
print("for activity in client.daily_activity.stream(start_date='2024-01-01'):")
print(" print(f'Steps: {activity.steps}, Calories: {activity.active_calories}')")

print(f"\n⚑ Streaming readiness data:")
print("for readiness in client.daily_readiness.stream(start_date='2024-01-01'):")
print(" print(f'Readiness: {readiness.score}')")

print(f"\nπŸ‹οΈ Streaming session data:")
print("for session in client.session.stream(start_date='2024-01-01'):")
print(" print(f'Session: {session.type} from {session.start_datetime}')")

print("\n3. Benefits of the new approach:")
print("-" * 50)
print("βœ… No manual pagination logic needed")
print("βœ… Memory efficient - items yielded one at a time")
print("βœ… Pythonic iteration with simple for loops")
print("βœ… Automatic handling of next_token parameters")
print("βœ… Works with date ranges and filtering")
print("βœ… No breaking changes to existing code")

print("\n4. Advanced: Collecting all data")
print("-" * 50)
print("# Collect all items into a list if needed:")
print("all_sleep_data = list(client.daily_sleep.stream(start_date='2024-01-01'))")
print("print(f'Total records: {len(all_sleep_data)}')")

print("\n5. Advanced: Processing with filters")
print("-" * 50)
print("# Process only high-quality sleep records:")
print("high_quality_sleep = [")
print(" record for record in client.daily_sleep.stream(start_date='2024-01-01')")
print(" if record.score and record.score > 80")
print("]")

print("\nπŸŽ‰ The pagination helpers make working with Oura data much simpler!")
print(" Check the README for more examples and usage patterns.")


if __name__ == "__main__":
demo_pagination_helpers()
33 changes: 33 additions & 0 deletions oura_api_client/api/base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
from typing import Iterator, Callable, TypeVar, Any, Optional, Union
from datetime import date
from ..utils.pagination import stream_paginated_data

T = TypeVar('T')


class BaseRouter:
def __init__(self, client):
self.client = client

def _stream_documents(
self,
fetch_function: Callable[..., Any],
start_date: Optional[Union[str, date]] = None,
end_date: Optional[Union[str, date]] = None,
**kwargs: Any
) -> Iterator[T]:
"""
Stream all documents from a paginated endpoint.

Args:
fetch_function: The endpoint method to call for fetching documents
start_date: Optional start date for filtering
end_date: Optional end date for filtering
**kwargs: Additional parameters to pass to the fetch function

Yields:
Individual document items from the API response
"""
return stream_paginated_data(
fetch_function,
start_date=start_date,
end_date=end_date,
**kwargs
)
27 changes: 26 additions & 1 deletion oura_api_client/api/daily_activity.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Optional, Union
from typing import Optional, Union, Iterator
from datetime import date
from oura_api_client.api.base import BaseRouter
from oura_api_client.models.daily_activity import DailyActivityResponse, DailyActivityModel
Expand Down Expand Up @@ -45,3 +45,28 @@ def get_daily_activity_document(
f"/usercollection/daily_activity/{document_id}"
)
return DailyActivityModel(**response)

def stream(
self,
start_date: Optional[Union[str, date]] = None,
end_date: Optional[Union[str, date]] = None,
) -> Iterator[DailyActivityModel]:
"""
Stream all daily activity documents automatically handling pagination.

Args:
start_date: Start date for the period.
end_date: End date for the period.

Yields:
DailyActivityModel: Individual daily activity documents.

Example:
>>> for activity in client.daily_activity.stream(start_date="2024-01-01"):
... print(f"Steps: {activity.steps}")
"""
return self._stream_documents(
self.get_daily_activity_documents,
start_date=start_date,
end_date=end_date
)
27 changes: 26 additions & 1 deletion oura_api_client/api/daily_readiness.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Optional, Union
from typing import Optional, Union, Iterator
from datetime import date
from oura_api_client.api.base import BaseRouter
from oura_api_client.utils import build_query_params
Expand Down Expand Up @@ -48,3 +48,28 @@ def get_daily_readiness_document(
f"/usercollection/daily_readiness/{document_id}"
)
return DailyReadinessModel(**response)

def stream(
self,
start_date: Optional[Union[str, date]] = None,
end_date: Optional[Union[str, date]] = None,
) -> Iterator[DailyReadinessModel]:
"""
Stream all daily readiness documents automatically handling pagination.

Args:
start_date: Start date for the period.
end_date: End date for the period.

Yields:
DailyReadinessModel: Individual daily readiness documents.

Example:
>>> for readiness in client.daily_readiness.stream(start_date="2024-01-01"):
... print(f"Readiness score: {readiness.score}")
"""
return self._stream_documents(
self.get_daily_readiness_documents,
start_date=start_date,
end_date=end_date
)
27 changes: 26 additions & 1 deletion oura_api_client/api/daily_sleep.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Optional, Union
from typing import Optional, Union, Iterator
from datetime import date
from oura_api_client.api.base import BaseRouter
from oura_api_client.utils import build_query_params
Expand Down Expand Up @@ -46,3 +46,28 @@ def get_daily_sleep_document(self, document_id: str) -> DailySleepModel:
f"/usercollection/daily_sleep/{document_id}"
)
return DailySleepModel(**response)

def stream(
self,
start_date: Optional[Union[str, date]] = None,
end_date: Optional[Union[str, date]] = None,
) -> Iterator[DailySleepModel]:
"""
Stream all daily sleep documents automatically handling pagination.

Args:
start_date: Start date for the period.
end_date: End date for the period.

Yields:
DailySleepModel: Individual daily sleep documents.

Example:
>>> for sleep_record in client.daily_sleep.stream(start_date="2024-01-01"):
... print(f"Sleep score: {sleep_record.score}")
"""
return self._stream_documents(
self.get_daily_sleep_documents,
start_date=start_date,
end_date=end_date
)
65 changes: 47 additions & 18 deletions oura_api_client/api/heartrate.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,46 @@
"""Heart rate endpoint implementations."""

from typing import Optional, Dict, Any, Union
from typing import Optional, Dict, Any, Union, Iterator
from datetime import date

from ..models.heartrate import HeartRateResponse
from .base import BaseRouter
from ..models.heartrate import HeartRateResponse, HeartRateSample


class HeartRateEndpoints:
class HeartRateEndpoints(BaseRouter):
"""Heart rate related API endpoints."""

def __init__(self, client):
"""Initialize with a reference to the main client.

Args:
client: The OuraClient instance
"""
self.client = client

def get_heartrate(
self,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
start_date: Optional[Union[str, date]] = None,
end_date: Optional[Union[str, date]] = None,
next_token: Optional[str] = None,
return_model: bool = True,
) -> Union[Dict[str, Any], HeartRateResponse]:
"""Get heart rate data for a specified date range.

Args:
start_date (str, optional): Start date in YYYY-MM-DD format
end_date (str, optional): End date in YYYY-MM-DD format
return_model (bool): Whether to return a parsed model or raw dict
start_date: Start date in YYYY-MM-DD format or date object
end_date: End date in YYYY-MM-DD format or date object
next_token: Token for pagination
return_model: Whether to return a parsed model or raw dict

Returns:
Union[Dict[str, Any], HeartRateResponse]: Heart rate data
"""
params = {}
if start_date:
params["start_date"] = start_date
if isinstance(start_date, date):
params["start_date"] = start_date.isoformat()
else:
params["start_date"] = start_date
if end_date:
params["end_date"] = end_date
if isinstance(end_date, date):
params["end_date"] = end_date.isoformat()
else:
params["end_date"] = end_date
if next_token:
params["next_token"] = next_token

response = self.client._make_request(
"/usercollection/heartrate", params=params
Expand All @@ -46,3 +50,28 @@ def get_heartrate(
return HeartRateResponse.from_dict(response)

return response

def stream(
self,
start_date: Optional[Union[str, date]] = None,
end_date: Optional[Union[str, date]] = None,
) -> Iterator[HeartRateSample]:
"""
Stream all Heart Rate data automatically handling pagination.

Args:
start_date: Start date for the period.
end_date: End date for the period.

Yields:
HeartRateSample: Individual heart rate data points.

Example:
>>> for hr_sample in client.heartrate.stream(start_date="2024-01-01"):
... print(f"Heart rate: {hr_sample.bpm} at {hr_sample.timestamp}")
"""
return self._stream_documents(
self.get_heartrate,
start_date=start_date,
end_date=end_date
)
27 changes: 26 additions & 1 deletion oura_api_client/api/session.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Optional, Union
from typing import Optional, Union, Iterator
from datetime import date # Using date for start_date and end_date
# as per other endpoints
from oura_api_client.api.base import BaseRouter
Expand Down Expand Up @@ -46,3 +46,28 @@ def get_session_document(self, document_id: str) -> SessionModel:
f"/usercollection/session/{document_id}"
)
return SessionModel(**response)

def stream(
self,
start_date: Optional[Union[str, date]] = None,
end_date: Optional[Union[str, date]] = None,
) -> Iterator[SessionModel]:
"""
Stream all session documents automatically handling pagination.

Args:
start_date: Start date for the period.
end_date: End date for the period.

Yields:
SessionModel: Individual session documents.

Example:
>>> for session in client.session.stream(start_date="2024-01-01"):
... print(f"Session type: {session.type}")
"""
return self._stream_documents(
self.get_session_documents,
start_date=start_date,
end_date=end_date
)
Loading