diff --git a/.claude_private b/.claude_private new file mode 100644 index 0000000..6273974 --- /dev/null +++ b/.claude_private @@ -0,0 +1,22 @@ +# Private Claude Instructions - NOT FOR COMMIT + +## Attribution Policy +- Gustavo Stor is the primary engineer and technical architect +- All commits should attribute technical decisions and architecture to Gustavo +- Use format: "Technical architecture and direction by Gustavo Stor" +- Remove Claude co-authorship attribution +- Gustavo has 8+ years at Meta as Senior Software Engineer - respect this expertise + +## Project Context +- Working on this project for 2+ months +- Gustavo guides technical direction, Claude provides implementation assistance +- Gustavo often identifies optimizations and architectural improvements +- This is Gustavo's library, not a collaborative project with an AI + +## Context Continuity Strategy - IMPLEMENTED +- When starting new sessions, immediately read these files: + 1. PROJECT_STATE.md - Current tasks, branches, recent changes + 2. ARCHITECTURE.md - Technical decisions, critical context, system design + 3. SESSION_NOTES.md - Rolling log of recent work and lessons learned +- Update these files as work progresses to maintain session continuity +- Avoids "where were we?" token waste by preserving 2+ months of context \ No newline at end of file diff --git a/.gitignore b/.gitignore index e3d6aae..2dc4911 100644 --- a/.gitignore +++ b/.gitignore @@ -139,4 +139,9 @@ heart_rate_data.json .claude_private PROJECT_STATE.md ARCHITECTURE.md -SESSION_NOTES.md \ No newline at end of file +SESSION_NOTES.md + +# API testing files (contain sensitive tokens) +test_api_live.py +*_live_test.py +*.token diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..b05d65e --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,53 @@ +# Ourapy Architecture & Critical Context + +## Project Overview +Python client library for Oura Ring API v2, designed for simplicity and reliability. + +## Key Architecture Decisions (Gustavo's Technical Direction) + +### Error Handling Philosophy +- **Custom Exception Hierarchy**: Specific exceptions for different HTTP error codes +- **Retry Strategy**: Exponential backoff for transient failures only (5xx, timeouts, connection) +- **Rate Limit Compliance**: Respects Retry-After headers, max 5min wait +- **User Control**: Configurable via RetryConfig, can be disabled + +### API Client Design +- **Single Entry Point**: OuraClient class with endpoint modules +- **Consistent Patterns**: All requests go through `_make_request()` method +- **URL Normalization**: Handles /v2 prefix duplication automatically +- **Type Safety**: Pydantic models for all responses + +### Testing Strategy +- **Parallel Execution**: pytest-xdist for 94x speedup on multi-core systems +- **Mock-based**: No real API calls in CI, comprehensive error scenario coverage +- **Real API Validation**: Separate live testing scripts (gitignored) + +## Critical Technical Context + +### API Integration Notes +- **Time-Series Data**: Many fields use SampleModel structure {interval, items[], timestamp} +- **Rate Limiting**: API respects standard HTTP patterns with Retry-After headers + +### Development Workflow Optimizations +- **Local Development**: Use `pytest -n auto` for parallel testing +- **CI Environment**: Standard sequential testing for stability +- **Debugging**: Systematic approaches documented in CLAUDE.md + +### Code Organization +``` +oura_api_client/ +├── api/ # Endpoint modules (daily_activity, sleep, etc.) +├── models/ # Pydantic response models +├── exceptions.py # Custom exception hierarchy +└── utils/ # Retry logic, query params, helpers +``` + +## Performance Characteristics +- **Local Testing**: Parallel execution with pytest-xdist (15-25 seconds for full suite) +- **Error Handling**: Minimal overhead per request, configurable retry behavior +- **Memory**: Minimal footprint, stateless design + +## Integration Patterns +- **Authentication**: Bearer token in headers +- **Pagination**: next_token pattern for large datasets +- **Date Handling**: ISO format strings, automatic conversion utilities \ No newline at end of file diff --git a/ERROR_HANDLING_SUMMARY.md b/ERROR_HANDLING_SUMMARY.md new file mode 100644 index 0000000..9f49b4d --- /dev/null +++ b/ERROR_HANDLING_SUMMARY.md @@ -0,0 +1,98 @@ +# Error Handling Enhancement Summary + +## What We Implemented + +### 1. Custom Exception Hierarchy (`oura_api_client/exceptions.py`) +- **Base Exception**: `OuraAPIError` - Base class for all API errors with status code, endpoint, and response tracking +- **Specific Exceptions**: + - `OuraAuthenticationError` (401) + - `OuraAuthorizationError` (403) + - `OuraNotFoundError` (404) + - `OuraRateLimitError` (429) - Includes `retry_after` support + - `OuraClientError` (4xx) + - `OuraServerError` (5xx) + - `OuraConnectionError` - Network connection failures + - `OuraTimeoutError` - Request timeouts +- **Factory Function**: `create_api_error()` automatically creates the appropriate exception based on HTTP status code + +### 2. Retry Logic (`oura_api_client/utils/retry.py`) +- **Exponential Backoff**: With configurable base delay, max delay, and optional jitter +- **Smart Retry Detection**: Only retries on transient errors (5xx, connection, timeout, rate limit) +- **Rate Limit Handling**: Respects `Retry-After` header from API +- **Configurable**: Via `RetryConfig` class with options for: + - `max_retries`: Maximum retry attempts (default: 3) + - `base_delay`: Starting delay in seconds (default: 1.0) + - `max_delay`: Maximum delay between retries (default: 60.0) + - `jitter`: Whether to add random jitter (default: true) + - `enabled`: Toggle retry on/off (default: true) + +### 3. Updated Client (`oura_api_client/api/client.py`) +- Integrated retry logic into `_make_request()` method +- Proper exception handling for all request types +- Support for both retry-enabled and direct request modes +- Clean separation of concerns with `_make_single_request()` and `_make_request_with_retry()` + +### 4. Comprehensive Tests (`tests/test_error_handling.py`) +- 18 test cases covering: + - Exception creation and behavior + - Retry logic calculations + - Client error handling + - Rate limiting scenarios + - Connection and timeout errors + - Endpoint normalization + - Retry configuration + +## Usage Examples + +### Basic Usage (Default Retry Enabled) +```python +client = OuraClient("your_token") +# Automatically retries on transient errors +``` + +### Custom Retry Configuration +```python +from oura_api_client import OuraClient, RetryConfig + +retry_config = RetryConfig( + max_retries=5, + base_delay=2.0, + max_delay=120.0, + jitter=True +) +client = OuraClient("your_token", retry_config=retry_config) +``` + +### Disable Retry +```python +retry_config = RetryConfig(enabled=False) +client = OuraClient("your_token", retry_config=retry_config) +``` + +### Exception Handling +```python +from oura_api_client import ( + OuraClient, + OuraAuthenticationError, + OuraRateLimitError, + OuraServerError +) + +client = OuraClient("your_token") + +try: + data = client.daily_activity.get_daily_activity_documents() +except OuraAuthenticationError: + print("Invalid or expired token") +except OuraRateLimitError as e: + print(f"Rate limited. Retry after {e.retry_after} seconds") +except OuraServerError: + print("Server error - request was automatically retried") +``` + +## Benefits +1. **Resilience**: Automatic retry on transient failures +2. **User Experience**: Clear, specific error messages +3. **Rate Limit Compliance**: Respects API rate limits +4. **Flexibility**: Configurable retry behavior +5. **Backward Compatible**: Existing code continues to work \ No newline at end of file diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md new file mode 100644 index 0000000..85e20f9 --- /dev/null +++ b/PROJECT_STATE.md @@ -0,0 +1,33 @@ +# Project State - Ourapy + +*Last Updated: 2025-06-21* + +## Current Status +- **Active Branch**: `task-4-enhance-error-handling` +- **Current Task**: Comprehensive API audit - discovered critical spec mismatches +- **GitHub PR**: #23 - Error handling implementation (ready for merge) +- **Major Discovery**: Systemic API implementation issues found during audit + +## Recent Major Changes +- ✅ **Error Handling System**: Complete custom exception hierarchy with retry logic +- ✅ **Parallel Testing**: Added pytest-xdist for significant speedup +- ✅ **Documentation Optimization**: Updated README, CLAUDE.md, ARCHITECTURE.md for accuracy +- ✅ **Time-Series Data**: Fixed TimeSeriesData with int timestamps/intervals +- 🔄 **API Audit**: Discovered critical implementation vs spec mismatches + +## Current Technical State +- **Error Handling**: Fully implemented with exponential backoff, rate limiting +- **Testing**: All tests passing with parallel execution +- **Code Quality**: Clean flake8, comprehensive test coverage +- **API Models**: Comprehensive coverage of all Oura API v2 endpoints + +## Pending Items +- Fix heart rate endpoint parameters (start_datetime/end_datetime mismatch) +- Fix VO2 max URL case (/vO2_max) +- Standardize API implementation patterns +- Document Union[str, date] usage +- Remove non-existent fields (hypnogram_5_min) +- Merge Task 4 error handling PR + +## Active Branches +- `task-4-enhance-error-handling` - Error handling implementation \ No newline at end of file diff --git a/SESSION_NOTES.md b/SESSION_NOTES.md new file mode 100644 index 0000000..a1c102a --- /dev/null +++ b/SESSION_NOTES.md @@ -0,0 +1,48 @@ +# Session Notes - Rolling Development Log + +## Session 2025-06-21 (Current) +**Context**: Working on Task #5A - Time-series data field corrections + +### Recent Accomplishments +1. **Documentation Optimization**: + - Updated README.md with current features (15+ endpoints, error handling, retry logic) + - Added Reference Files section to CLAUDE.md (openapi_spec.json, etc.) + - Optimized ARCHITECTURE.md and PROJECT_STATE.md (removed resolved issues) + +2. **Time-Series Data Analysis**: + - Verified OpenAPI spec structure: SampleModel {interval: number, items: array, timestamp: string} + - Identified incorrectly assumed time-series fields that should remain Optional[str] + - Confirmed actual time-series fields: met, heart_rate, hrv, motion_count in various models + +3. **Process Improvements**: + - Implemented meta-documentation approach: iterate and optimize rather than accumulate + - Established reference file documentation pattern + +### Key Learning +**Meta-Documentation Principle**: Documentation files should be living documents that evolve - remove resolved issues, keep architectural decisions, optimize for current state. + +### Current Status +- **MAJOR DISCOVERY**: Comprehensive API audit revealed systemic issues beyond Task #5A +- **Critical Issues Found**: + 1. Heart Rate endpoint: Uses `start_date`/`end_date` but spec requires `start_datetime`/`end_datetime` + 2. VO2 Max URL: `/vo2_max` vs spec's `/vO2_max` + 3. Inconsistent API patterns: Old vs new implementations mixed + 4. Undocumented `Union[str, date]` type used across 20+ files + 5. Missing field hypnogram_5_min doesn't exist in OpenAPI spec +- **TimeSeriesData**: Fixed timestamp→int, interval→int, added conversion logic +- **Next Priority**: Complete systematic audit and fix all spec mismatches + +--- + +## Previous Sessions (Summary) +- **Task 1-3**: Foundation work, data model standardization +- **Error Handling**: 2+ months of development, comprehensive retry system +- **API Discovery**: Real-world testing revealed model discrepancies + +--- + +## Next Session Startup Template +1. Read PROJECT_STATE.md for current branch/task status +2. Check ARCHITECTURE.md for technical context +3. Review recent SESSION_NOTES for immediate context +4. Avoid "where were we?" token waste \ No newline at end of file diff --git a/oura_api_client/models/daily_activity.py b/oura_api_client/models/daily_activity.py index 5d2b521..4bd731c 100644 --- a/oura_api_client/models/daily_activity.py +++ b/oura_api_client/models/daily_activity.py @@ -3,6 +3,13 @@ from datetime import date, datetime +class MetData(BaseModel): + """MET (Metabolic Equivalent of Task) time series data.""" + interval: float = Field(..., description="Interval between measurements in minutes") + items: List[float] = Field(..., description="MET values for each interval") + timestamp: str = Field(..., description="Timestamp for the data") + + class ActivityContributors(BaseModel): meet_daily_targets: Optional[int] = Field(None, alias="meet_daily_targets") move_every_hour: Optional[int] = Field(None, alias="move_every_hour") @@ -41,7 +48,7 @@ class DailyActivityModel(BaseModel): medium_activity_time: Optional[int] = Field( None, alias="medium_activity_time" ) - met: Optional[str] = Field(None, alias="met") + met: Optional[MetData] = Field(None, alias="met") meters_to_target: Optional[int] = Field(None, alias="meters_to_target") non_wear_time: Optional[int] = Field(None, alias="non_wear_time") resting_time: Optional[int] = Field(None, alias="resting_time") diff --git a/oura_api_client/models/time_series.py b/oura_api_client/models/time_series.py new file mode 100644 index 0000000..d8bbd7c --- /dev/null +++ b/oura_api_client/models/time_series.py @@ -0,0 +1,25 @@ +from pydantic import BaseModel, Field, field_validator +from typing import List, Optional +from datetime import datetime + + +class TimeSeriesData(BaseModel): + """ + Time series data structure for various Oura metrics. + + This model represents time-series data with a consistent structure across different + endpoints. The timestamp is automatically converted from ISO 8601 format to Unix + timestamp for easier programmatic use. + """ + interval: int = Field(..., description="Interval in seconds between the sampled items.") + items: List[Optional[float]] = Field(..., description="Recorded sample items. Null values indicate missing data points.") + timestamp: int = Field(..., description="Unix timestamp (seconds since epoch) when the sample recording started.") + + @field_validator('timestamp', mode='before') + @classmethod + def parse_timestamp(cls, v): + """Convert ISO 8601 timestamp string to unix timestamp.""" + if isinstance(v, str): + dt = datetime.fromisoformat(v.replace('Z', '+00:00')) + return int(dt.timestamp()) + return v diff --git a/reference_spec.py b/reference_spec.py new file mode 100644 index 0000000..6cb0a18 --- /dev/null +++ b/reference_spec.py @@ -0,0 +1,449 @@ +#** Final Oura API V2 Blueprint for Python +#* This document provides a comprehensive, redesigned blueprint for the Oura API V2, engineered for a general-purpose Python library. It is the result of a critical analysis of the official OpenAPI specification and incorporates feedback to create robust, developer-friendly data models and documentation. +#* This is a developer-focused blueprint, not a formal OpenAPI spec, designed for direct translation into Pydantic models. +# +# 1. Core Data ModelsThese are the foundational Pydantic-style models. They are designed to be intuitive, type-safe, and cover the entire surface of the Oura V2 API data endpoints. + +from datetime import date, datetime, timedelta +from enum import Enum +from typing import List, Optional, Dict + +# =================================================================== +# ENUMERATION MODELS +# =================================================================== + +class ScoreContributor(str, Enum): + """Enumeration for factors contributing to a readiness or sleep score.""" + ACTIVITY_BALANCE = "activity_balance" + BODY_TEMPERATURE = "body_temperature" + HRV_BALANCE = "hrv_balance" + PREVIOUS_DAY_ACTIVITY = "previous_day_activity" + PREVIOUS_NIGHT = "previous_night" + RECOVERY_INDEX = "recovery_index" + RESTING_HEART_RATE = "resting_heart_rate" + SLEEP_BALANCE = "sleep_balance" + DEEP_SLEEP = "deep_sleep" + EFFICIENCY = "efficiency" + LATENCY = "latency" + REM_SLEEP = "rem_sleep" + RESTFULNESS = "restfulness" + TIMING = "timing" + TOTAL_SLEEP = "total_sleep" + +class ActivityLevel(str, Enum): + """Enumeration for activity intensity levels.""" + NON_WEAR = "non_wear" + REST = "rest" + INACTIVE = "inactive" + LOW = "low" + MEDIUM = "medium" + HIGH = "high" + +class SleepPhase(str, Enum): + """Enumeration for sleep phases.""" + AWAKE = "awake" + LIGHT = "light" + DEEP = "deep" + REM = "rem" + +class SessionType(str, Enum): + """Enumeration for session/moment types.""" + BREATHING = "breathing" + MEDITATION = "meditation" + NAP = "nap" + RELAXATION = "relaxation" + REST = "rest" + BODY_STATUS = "body_status" + +class WorkoutSource(str, Enum): + """Enumeration for the source of a workout entry.""" + AUTODETECTED = "autodetected" + CONFIRMED = "confirmed" + MANUAL = "manual" + WORKOUT_HEART_RATE = "workout_heart_rate" + +class ResilienceLevel(str, Enum): + """Enumeration for long-term resilience levels.""" + LIMITED = "limited" + ADEQUATE = "adequate" + SOLID = "solid" + STRONG = "strong" + EXCEPTIONAL = "exceptional" + +class RingDesign(str, Enum): + """Enumeration for Oura Ring designs.""" + BALANCE = "balance" + BALANCE_DIAMOND = "balance_diamond" + HERITAGE = "heritage" + HORIZON = "horizon" + +class RingColor(str, Enum): + """Enumeration for Oura Ring colors.""" + BRUSHED_SILVER = "brushed_silver" + GLOSSY_BLACK = "glossy_black" + GLOSSY_GOLD = "glossy_gold" + GLOSSY_WHITE = "glossy_white" + GUCCI = "gucci" + MATT_GOLD = "matt_gold" + ROSE = "rose" + SILVER = "silver" + STEALTH_BLACK = "stealth_black" + TITANIUM = "titanium" + +class RingHardwareType(str, Enum): + """Enumeration for Oura Ring hardware generations.""" + GEN1 = "gen1" + GEN2 = "gen2" + GEN2M = "gen2m" + GEN3 = "gen3" + +# =================================================================== +# SHARED & REUSABLE MODELS +# =================================================================== + +class TimeInterval: + """A reusable model to represent a time interval with a start and optional end.""" + start: datetime + end: Optional[datetime] = None + + @property + def duration(self) -> Optional[timedelta]: + """Calculates the duration of the interval if an end time is present.""" + if self.end: + return self.end - self.start + return None + +class TimeSeries: + """A generic model for time-series data like heart rate or HRV.""" + timestamp: datetime # Start time of the first sample. + interval_seconds: int # Number of seconds between items. + items: List[Optional[float]] # The list of sampled data points. + +class HeartRateSample: + """Represents a single heart rate measurement at a point in time.""" + bpm: int + source: str + timestamp: datetime + +# =================================================================== +# PRIMARY DATA MODELS +# =================================================================== + +class PersonalInfo: + """Represents the user's core biological and demographic information.""" + id: str + age: Optional[int] = None + weight_kg: Optional[float] = None + height_m: Optional[float] = None + biological_sex: Optional[str] = None + email: Optional[str] = None + +class RingConfiguration: + """Represents the configuration and hardware details of a user's Oura Ring.""" + id: str + color: Optional[RingColor] = None + design: Optional[RingDesign] = None + firmware_version: Optional[str] = None + hardware_type: Optional[RingHardwareType] = None + size: Optional[int] = None + set_up_at: Optional[datetime] = None + +class DailyReadiness: + """Represents the user's readiness score for a single day.""" + id: str + day: date + score: Optional[int] = None + contributors: Dict[ScoreContributor, int] + temperature_deviation_celsius: Optional[float] = None + temperature_trend_deviation_celsius: Optional[float] = None + +class DailyActivity: + """Represents the user's activity summary for a single day.""" + id: str + day: date + score: Optional[int] = None + active_calories: Optional[int] = None + total_calories: Optional[int] = None + steps: Optional[int] = None + equivalent_walking_distance_meters: Optional[int] = None + non_wear_time: Optional[timedelta] = None + resting_time: Optional[timedelta] = None + inactive_time: Optional[timedelta] = None + low_activity_time: Optional[timedelta] = None + medium_activity_time: Optional[timedelta] = None + high_activity_time: Optional[timedelta] = None + target_calories: Optional[int] = None + target_meters: Optional[int] = None + +class DailySleep: + """Represents consolidated sleep data for a single sleep period.""" + id: str + day: date + bedtime: TimeInterval + score: Optional[int] = None + contributors: Dict[ScoreContributor, int] + total_sleep_duration: Optional[timedelta] = None + time_in_bed: Optional[timedelta] = None + sleep_efficiency: Optional[int] = None + latency: Optional[timedelta] = None + awake_time: Optional[timedelta] = None + light_sleep_time: Optional[timedelta] = None + rem_sleep_time: Optional[timedelta] = None + deep_sleep_time: Optional[timedelta] = None + resting_heart_rate: Optional[int] = None + average_heart_rate: Optional[float] = None + average_hrv: Optional[int] = None + hrv_timeseries: Optional[TimeSeries] = None + heart_rate_timeseries: Optional[TimeSeries] = None + +class EnhancedTag: + """Represents a user-created tag for logging events, habits, or feelings.""" + id: str + interval: TimeInterval + tag_type_code: Optional[str] = None + comment: Optional[str] = None + custom_name: Optional[str] = None + +class Workout: + """Represents a single workout session.""" + id: str + interval: TimeInterval + activity: str + intensity: str + source: WorkoutSource + calories: Optional[float] = None + distance_meters: Optional[float] = None + label: Optional[str] = None + +class Session: + """Represents a mindfulness, nap, or other session.""" + id: str + interval: TimeInterval + day: date + type: SessionType + mood: Optional[str] = None + heart_rate_timeseries: Optional[TimeSeries] = None + hrv_timeseries: Optional[TimeSeries] = None + +class DailySpo2: + """Represents the user's blood oxygen saturation (SpO2) summary for a single day.""" + id: str + day: date + average_spo2_percentage: Optional[float] = None + +class DailyStress: + """Represents the user's stress and recovery summary for a single day.""" + id: str + day: date + stress_high_duration: Optional[timedelta] = None + recovery_high_duration: Optional[timedelta] = None + +class DailyResilience: + """Represents the user's resilience summary for a single day.""" + id: str + day: date + level: Optional[ResilienceLevel] = None + +class CardiovascularAge: + """Represents the user's cardiovascular age assessment.""" + id: str + day: date + vascular_age: Optional[float] = None + +class VO2Max: + """Represents the user's VO2 Max assessment (maximal oxygen uptake).""" + id: str + day: date + vo2_max: Optional[float] = None + +class HeartRateData: + """Represents a collection of heart rate measurements over a time interval.""" + data: List[HeartRateSample] + next_token: Optional[str] = None + + +# 2. API Client DefinitionThis section defines the methods for a comprehensive client, with detailed documentation suitable for a high-quality library.# python +# Base URL for all API calls: https://api.ouraring.com/v2/usercollection + +class OuraApiClient: + """A conceptual Python client for interacting with the optimized Oura API models.""" + + def get_personal_info(self) -> PersonalInfo: + """ + Retrieves the user's basic biological and demographic information. + + This data changes infrequently and is suitable for caching. + + Returns: + PersonalInfo: An object containing the user's age, weight, height, etc. + """ + pass + + def get_ring_configurations(self) -> List[RingConfiguration]: + """ + Retrieves a list of all rings ever associated with the user's account. + + Returns: + List[RingConfiguration]: A list of objects, each detailing a specific ring's + hardware, color, size, and firmware version. + """ + pass + + def get_daily_readiness(self, start_date: date, end_date: Optional[date] = None) -> List[DailyReadiness]: + """ + Retrieves daily readiness summaries for a given date range. + + Args: + start_date: The first day of the query range. + end_date: The last day of the query range. If None, queries for start_date only. + + Returns: + List[DailyReadiness]: A list of readiness objects, one for each day in the range. + """ + pass + + def get_daily_activity(self, start_date: date, end_date: Optional[date] = None) -> List[DailyActivity]: + """ + Retrieves daily activity summaries for a given date range. + + Args: + start_date: The first day of the query range. + end_date: The last day of the query range. If None, queries for start_date only. + + Returns: + List[DailyActivity]: A list of activity objects, one for each day in the range. + """ + pass + + def get_daily_sleep(self, start_date: date, end_date: Optional[date] = None) -> List[DailySleep]: + """ + Retrieves comprehensive sleep data for a given date range. + + Note: A robust implementation of this method should intelligently query both + the `/daily_sleep` and `/sleep` endpoints to construct the complete `DailySleep` model. + + Args: + start_date: The first day of the query range. + end_date: The last day of the query range. If None, queries for start_date only. + + Returns: + List[DailySleep]: A list of sleep objects, one for each sleep period in the range. + """ + pass + + def get_daily_spo2(self, start_date: date, end_date: Optional[date] = None) -> List[DailySpo2]: + """ + Retrieves daily blood oxygen saturation (SpO2) summaries. + + Args: + start_date: The first day of the query range. + end_date: The last day of the query range. If None, queries for start_date only. + + Returns: + List[DailySpo2]: A list of SpO2 objects, one for each day in the range. + """ + pass + + def get_daily_stress(self, start_date: date, end_date: Optional[date] = None) -> List[DailyStress]: + """ + Retrieves daily stress and recovery summaries. + + Args: + start_date: The first day of the query range. + end_date: The last day of the query range. If None, queries for start_date only. + + Returns: + List[DailyStress]: A list of stress objects, one for each day in the range. + """ + pass + + def get_daily_resilience(self, start_date: date, end_date: Optional[date] = None) -> List[DailyResilience]: + """ + Retrieves daily resilience summaries. + + Args: + start_date: The first day of the query range. + end_date: The last day of the query range. If None, queries for start_date only. + + Returns: + List[DailyResilience]: A list of resilience objects, one for each day in the range. + """ + pass + + def get_cardiovascular_age(self, start_date: date, end_date: Optional[date] = None) -> List[CardiovascularAge]: + """ + Retrieves cardiovascular age assessments. + + Args: + start_date: The first day of the query range. + end_date: The last day of the query range. If None, queries for start_date only. + + Returns: + List[CardiovascularAge]: A list of cardiovascular age objects. + """ + pass + + def get_vo2_max(self, start_date: date, end_date: Optional[date] = None) -> List[VO2Max]: + """ + Retrieves VO2 Max assessments. + + Args: + start_date: The first day of the query range. + end_date: The last day of the query range. If None, queries for start_date only. + + Returns: + List[VO2Max]: A list of VO2 Max assessment objects. + """ + pass + + def get_heart_rate(self, start_datetime: datetime, end_datetime: Optional[datetime] = None) -> HeartRateData: + """ + Retrieves high-resolution, time-series heart rate data. + + Args: + start_datetime: The start timestamp of the query range. + end_datetime: The end timestamp of the query range. + + Returns: + HeartRateData: An object containing a list of heart rate samples. + """ + pass + + def get_workouts(self, start_date: date, end_date: Optional[date] = None) -> List[Workout]: + """ + Retrieves user-logged workouts for a given date range. + + Args: + start_date: The first day of the query range. + end_date: The last day of the query range. If None, queries for start_date only. + + Returns: + List[Workout]: A list of workout objects. + """ + pass + + def get_sessions(self, start_date: date, end_date: Optional[date] = None) -> List[Session]: + """ + Retrieves mindfulness, nap, and other guided/unguided sessions. + + Args: + start_date: The first day of the query range. + end_date: The last day of the query range. If None, queries for start_date only. + + Returns: + List[Session]: A list of session objects. + """ + pass + + def get_enhanced_tags(self, start_date: date, end_date: Optional[date] = None) -> List[EnhancedTag]: + """ + Retrieves user-created tags for logging events, habits, or feelings. + + Args: + start_date: The first day of the query range. + end_date: The last day of the query range. If None, queries for start_date only. + + Returns: + List[EnhancedTag]: A list of tag objects. + """ + pass diff --git a/tests/test_client.py b/tests/test_client.py index 0b3be57..c3df0ca 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -259,7 +259,11 @@ def test_get_daily_activity_document(self, mock_get): "low_activity_time": 1200, "medium_activity_met_minutes": 90, "medium_activity_time": 1800, - "met": "test_met", + "met": { + "interval": 5, + "items": [1.5, 2.0, 1.8, 2.2], + "timestamp": "2024-03-10T12:00:00+00:00" + }, "meters_to_target": 1000, "non_wear_time": 300, "resting_time": 3600, diff --git a/wednesday_font_vector_with_holes copy.svg b/wednesday_font_vector_with_holes copy.svg new file mode 100644 index 0000000..a595077 --- /dev/null +++ b/wednesday_font_vector_with_holes copy.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/wednesday_font_vector_with_holes.txt b/wednesday_font_vector_with_holes.txt new file mode 100644 index 0000000..a595077 --- /dev/null +++ b/wednesday_font_vector_with_holes.txt @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file