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
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,10 @@ dmypy.json
.DS_Store

# Project specific files
heart_rate_data.json
heart_rate_data.json

# Private Claude instructions and context
.claude_private
PROJECT_STATE.md
ARCHITECTURE.md
SESSION_NOTES.md
112 changes: 112 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Ourapy Project Instructions

## Meta Instructions - IMPORTANT
- **Continuously improve these instructions** as we develop the library
- **Proactively suggest updates** when you notice:
- Repeated patterns that should be documented
- Better ways to accomplish tasks
- Outdated practices that need updating
- Missing guidelines that would improve consistency
- **Remove or update** instructions that no longer apply
- **Add new sections** as we explore new features or adopt new practices
- **Track significant changes** in the Change Log section at the bottom
- Consider these instructions as living documentation that evolves with the project

### When to Update CLAUDE.md
- After implementing a new feature that introduces patterns
- When we establish a new best practice through experience
- After resolving issues that could be prevented with better guidelines
- When external dependencies or tools change
- During code reviews when we identify improvement opportunities
- When adopting new state-of-the-art practices

### Context File Strategy
**Session Startup Protocol**: When detecting context loss (new session, after auto-compact, or when asked about something I should know but don't), immediately read all context files in one batch:
1. PROJECT_STATE.md - Current status and active work
2. ARCHITECTURE.md - Technical decisions and critical context
3. SESSION_NOTES.md - Recent development history

**During Session**: Keep these files updated as work progresses, but don't re-read unless detecting another context loss event (WHICH SHOULD BE RARE).

**Mid-Session Re-reading Threshold**: Only re-read context files when ALL conditions met:
- Confidence I should know the answer: >85%
- Confidence in my actual answer: <25%
- Messages since last context read: >20
- Alternative: Ask explicitly "Should I re-read context files?" when uncertain

**Optimization**: Read all relevant context files at once rather than piecemeal to minimize token overhead.

## Code Style
- Use 4 spaces for indentation (Python PEP 8)
- Always run flake8 before considering any task complete
- Maximum line length: 100 characters
- Use type hints for all function parameters and return values

## Testing
- Write tests for all new functionality
- Run pytest before marking any task as complete
- Test files should mirror the source structure in the tests/ directory
- Use descriptive test names that explain what's being tested

## Error Handling
- Use the custom exception hierarchy in oura_api_client.exceptions
- Always provide meaningful error messages
- Implement retry logic for transient failures (5xx, timeouts, connection errors)

## Git Workflow
- Never commit directly unless explicitly asked
- Always check git status before making changes
- Create descriptive commit messages explaining the "why" not just the "what"

## Documentation
- Update docstrings for any modified functions
- Use Google-style docstrings
- Include usage examples for public APIs

## Project-Specific Commands
- Lint: `flake8 oura_api_client/ tests/`
- Test (parallel): `python -m pytest tests/ -n auto -v`
- Test (sequential): `python -m pytest tests/ -v`
- Test (specific): `python -m pytest tests/test_specific.py::TestClass::test_method -v`
- Type check: `mypy oura_api_client/` (if available)

## API Design Principles
- Keep the client interface simple and intuitive
- Use Pydantic models for all API responses
- Maintain backward compatibility when possible

## Common Patterns
- All API endpoints should go through the `_make_request` method
- Use the `build_query_params` utility for consistent parameter handling
- Follow the existing endpoint module pattern for new features

## Debugging Test Failures
**IMPORTANT**: Use systematic approaches when tests fail, especially in CI environments.

### Local Debugging Strategy
1. **Use parallel execution**: `pytest -n auto` for faster feedback
2. **Target specific failures**: `pytest -x --tb=short` (stop on first failure)
3. **Re-run only failed tests**: `pytest --lf` (last failed)
4. **Isolate by class/method**: `pytest tests/test_file.py::TestClass::test_method`
5. **Use short tracebacks**: `--tb=short` or `--tb=line` for cleaner output

### CI Failure Investigation
- **Timeouts ≠ No failures**: Test timeouts can mask actual test failures
- **Ask for specific error output** when remote logs aren't accessible
- **Don't assume environmental issues** without evidence
- **Test locally first** with the same conditions when possible

### Common Patterns to Watch For
- Mock incompatibilities after refactoring (e.g., `raise_for_status` vs `response.ok`)
- Import errors from new dependencies
- Pydantic model validation failures from API changes

## Change Log
### 2025-06-21
- Added Meta Instructions section to ensure continuous improvement of guidelines
- Added parallel testing with pytest-xdist for faster local development
- Added comprehensive debugging section with systematic test failure approaches
- Updated project commands to use parallel testing by default
- Added context file strategy with threshold-based re-reading to minimize token waste
- Implemented session continuity system with PROJECT_STATE.md, ARCHITECTURE.md, SESSION_NOTES.md
- Initial creation with sections for code style, testing, error handling, git workflow, documentation, commands, API design, and common patterns
28 changes: 28 additions & 0 deletions oura_api_client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
"""Oura API Client - A Python library for the Oura Ring API."""

from .api.client import OuraClient
from .exceptions import (
OuraAPIError,
OuraAuthenticationError,
OuraAuthorizationError,
OuraNotFoundError,
OuraRateLimitError,
OuraServerError,
OuraClientError,
OuraConnectionError,
OuraTimeoutError
)
from .utils import RetryConfig

__version__ = "0.1.0"

__all__ = [
"OuraClient",
"OuraAPIError",
"OuraAuthenticationError",
"OuraAuthorizationError",
"OuraNotFoundError",
"OuraRateLimitError",
"OuraServerError",
"OuraClientError",
"OuraConnectionError",
"OuraTimeoutError",
"RetryConfig"
]
95 changes: 88 additions & 7 deletions oura_api_client/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import requests
from typing import Optional, Dict, Any

from ..exceptions import create_api_error, OuraConnectionError, OuraTimeoutError
from ..utils import RetryConfig, retry_with_backoff

from .heartrate import HeartRateEndpoints
from .personal import PersonalEndpoints
from .daily_activity import DailyActivity
Expand All @@ -29,17 +32,19 @@ class OuraClient:

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

def __init__(self, access_token: str):
def __init__(self, access_token: str, retry_config: Optional[RetryConfig] = 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
"""
self.access_token = access_token
self.headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}
self.retry_config = retry_config or RetryConfig()

# Initialize endpoint modules
self.heartrate = HeartRateEndpoints(self)
Expand Down Expand Up @@ -67,19 +72,21 @@ def _make_request(
endpoint: str,
params: Optional[Dict[str, Any]] = None,
method: str = "GET",
timeout: Optional[float] = 30.0,
) -> Dict[str, Any]:
"""Make a request to the Oura API.

Args:
endpoint (str): The API endpoint to call (should start with /)
params (dict, optional): Query parameters for the request
method (str): HTTP method to use (default: GET)
timeout (float, optional): Request timeout in seconds

Returns:
dict: The JSON response from the API

Raises:
requests.exceptions.RequestException: If the API request fails
OuraAPIError: If the API request fails with specific error details
"""
# Ensure endpoint starts with /
if not endpoint.startswith('/'):
Expand All @@ -91,10 +98,84 @@ def _make_request(

url = f"{self.BASE_URL}{endpoint}"

if method.upper() == "GET":
response = requests.get(url, headers=self.headers, params=params)
# 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)
else:
raise ValueError(f"HTTP method {method} is not supported yet")
return self._make_single_request(url, method, params, timeout, endpoint)

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

Args:
url: Full URL to request
method: HTTP method
params: Query parameters
timeout: Request timeout
endpoint: Original endpoint for error context

Returns:
dict: The JSON response from the API

Raises:
OuraAPIError: If the request fails
"""
try:
if method.upper() == "GET":
response = requests.get(url, headers=self.headers, params=params, timeout=timeout)
else:
raise ValueError(f"HTTP method {method} is not supported yet")

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

response.raise_for_status()
return response.json()
return response.json()

except requests.exceptions.Timeout as e:
raise OuraTimeoutError(f"Request timed out after {timeout} seconds", endpoint=endpoint) from e
except requests.exceptions.ConnectionError as e:
raise OuraConnectionError(f"Failed to connect to API: {str(e)}", endpoint=endpoint) from e
except requests.exceptions.RequestException as e:
raise create_api_error(getattr(e, 'response', None), endpoint, str(e)) from e

def _make_request_with_retry(
self,
url: str,
method: str,
params: Optional[Dict[str, Any]],
timeout: Optional[float],
endpoint: str
) -> Dict[str, Any]:
"""Make HTTP request with retry logic.

Args:
url: Full URL to request
method: HTTP method
params: Query parameters
timeout: Request timeout
endpoint: Original endpoint for error context

Returns:
dict: The JSON response from the API

Raises:
OuraAPIError: If all retries fail
"""
@retry_with_backoff(
max_retries=self.retry_config.max_retries,
base_delay=self.retry_config.base_delay,
max_delay=self.retry_config.max_delay,
jitter=self.retry_config.jitter
)
def make_request():
return self._make_single_request(url, method, params, timeout, endpoint)

return make_request()
Loading