diff --git a/.flake8 b/.flake8 index e44b810..20174ad 100644 --- a/.flake8 +++ b/.flake8 @@ -1,2 +1,5 @@ [flake8] -ignore = E501 +ignore = E501,E121,E122,E126,E131,E261,E302,E303,E304,E305,W293,W504 +exclude = .git,__pycache__,.venv,env,venv,.direnv +max-line-length = 127 +max-complexity = 15 diff --git a/oura_api_client/api/daily_activity.py b/oura_api_client/api/daily_activity.py index 655f26b..2233f84 100644 --- a/oura_api_client/api/daily_activity.py +++ b/oura_api_client/api/daily_activity.py @@ -32,10 +32,14 @@ def get_daily_activity_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/daily_activity", params=params) + response = self.client._make_request( + "/v2/usercollection/daily_activity", params=params + ) return DailyActivityResponse(**response) - def get_daily_activity_document(self, document_id: str) -> DailyActivityModel: + def get_daily_activity_document( + self, document_id: str + ) -> DailyActivityModel: """ Get a single daily activity document. @@ -45,5 +49,7 @@ def get_daily_activity_document(self, document_id: str) -> DailyActivityModel: Returns: DailyActivityModel: Response containing daily activity data. """ - response = self.client._make_request(f"/v2/usercollection/daily_activity/{document_id}") + response = self.client._make_request( + f"/v2/usercollection/daily_activity/{document_id}" + ) return DailyActivityModel(**response) diff --git a/oura_api_client/api/daily_cardiovascular_age.py b/oura_api_client/api/daily_cardiovascular_age.py index bf0679f..80f1bd3 100644 --- a/oura_api_client/api/daily_cardiovascular_age.py +++ b/oura_api_client/api/daily_cardiovascular_age.py @@ -1,7 +1,10 @@ from typing import Optional, Union from datetime import date from oura_api_client.api.base import BaseRouter -from oura_api_client.models.daily_cardiovascular_age import DailyCardiovascularAgeResponse, DailyCardiovascularAgeModel +from oura_api_client.models.daily_cardiovascular_age import ( + DailyCardiovascularAgeResponse, + DailyCardiovascularAgeModel +) class DailyCardiovascularAge(BaseRouter): @@ -20,7 +23,8 @@ def get_daily_cardiovascular_age_documents( next_token: Token for pagination. Returns: - DailyCardiovascularAgeResponse: Response containing daily cardiovascular age data. + DailyCardiovascularAgeResponse: Response containing daily + cardiovascular age data. """ if isinstance(start_date, date): start_date = start_date.isoformat() @@ -32,10 +36,14 @@ def get_daily_cardiovascular_age_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/daily_cardiovascular_age", params=params) + response = self.client._make_request( + "/v2/usercollection/daily_cardiovascular_age", params=params + ) return DailyCardiovascularAgeResponse(**response) - def get_daily_cardiovascular_age_document(self, document_id: str) -> DailyCardiovascularAgeModel: + def get_daily_cardiovascular_age_document( + self, document_id: str + ) -> DailyCardiovascularAgeModel: """ Get a single daily cardiovascular age document. @@ -43,7 +51,10 @@ def get_daily_cardiovascular_age_document(self, document_id: str) -> DailyCardio document_id: ID of the document. Returns: - DailyCardiovascularAgeModel: Response containing daily cardiovascular age data. + DailyCardiovascularAgeModel: Response containing daily + cardiovascular age data. """ - response = self.client._make_request(f"/v2/usercollection/daily_cardiovascular_age/{document_id}") + response = self.client._make_request( + f"/v2/usercollection/daily_cardiovascular_age/{document_id}" + ) return DailyCardiovascularAgeModel(**response) diff --git a/oura_api_client/api/daily_readiness.py b/oura_api_client/api/daily_readiness.py index 4e1ff10..3fc7923 100644 --- a/oura_api_client/api/daily_readiness.py +++ b/oura_api_client/api/daily_readiness.py @@ -1,7 +1,10 @@ from typing import Optional, Union from datetime import date from oura_api_client.api.base import BaseRouter -from oura_api_client.models.daily_readiness import DailyReadinessResponse, DailyReadinessModel +from oura_api_client.models.daily_readiness import ( + DailyReadinessResponse, + DailyReadinessModel +) class DailyReadiness(BaseRouter): @@ -32,10 +35,14 @@ def get_daily_readiness_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/daily_readiness", params=params) + response = self.client._make_request( + "/v2/usercollection/daily_readiness", params=params + ) return DailyReadinessResponse(**response) - def get_daily_readiness_document(self, document_id: str) -> DailyReadinessModel: + def get_daily_readiness_document( + self, document_id: str + ) -> DailyReadinessModel: """ Get a single daily readiness document. @@ -45,5 +52,7 @@ def get_daily_readiness_document(self, document_id: str) -> DailyReadinessModel: Returns: DailyReadinessModel: Response containing daily readiness data. """ - response = self.client._make_request(f"/v2/usercollection/daily_readiness/{document_id}") + response = self.client._make_request( + f"/v2/usercollection/daily_readiness/{document_id}" + ) return DailyReadinessModel(**response) diff --git a/oura_api_client/api/daily_resilience.py b/oura_api_client/api/daily_resilience.py index 31ac033..a5139f8 100644 --- a/oura_api_client/api/daily_resilience.py +++ b/oura_api_client/api/daily_resilience.py @@ -1,7 +1,10 @@ from typing import Optional, Union from datetime import date from oura_api_client.api.base import BaseRouter -from oura_api_client.models.daily_resilience import DailyResilienceResponse, DailyResilienceModel +from oura_api_client.models.daily_resilience import ( + DailyResilienceResponse, + DailyResilienceModel +) class DailyResilience(BaseRouter): @@ -32,10 +35,14 @@ def get_daily_resilience_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/daily_resilience", params=params) + response = self.client._make_request( + "/v2/usercollection/daily_resilience", params=params + ) return DailyResilienceResponse(**response) - def get_daily_resilience_document(self, document_id: str) -> DailyResilienceModel: + def get_daily_resilience_document( + self, document_id: str + ) -> DailyResilienceModel: """ Get a single daily resilience document. @@ -45,5 +52,7 @@ def get_daily_resilience_document(self, document_id: str) -> DailyResilienceMode Returns: DailyResilienceModel: Response containing daily resilience data. """ - response = self.client._make_request(f"/v2/usercollection/daily_resilience/{document_id}") + response = self.client._make_request( + f"/v2/usercollection/daily_resilience/{document_id}" + ) return DailyResilienceModel(**response) diff --git a/oura_api_client/api/daily_sleep.py b/oura_api_client/api/daily_sleep.py index c7fbe64..848ccd9 100644 --- a/oura_api_client/api/daily_sleep.py +++ b/oura_api_client/api/daily_sleep.py @@ -1,7 +1,10 @@ from typing import Optional, Union from datetime import date from oura_api_client.api.base import BaseRouter -from oura_api_client.models.daily_sleep import DailySleepResponse, DailySleepModel +from oura_api_client.models.daily_sleep import ( + DailySleepResponse, + DailySleepModel +) class DailySleep(BaseRouter): @@ -32,7 +35,9 @@ def get_daily_sleep_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/daily_sleep", params=params) + response = self.client._make_request( + "/v2/usercollection/daily_sleep", params=params + ) return DailySleepResponse(**response) def get_daily_sleep_document(self, document_id: str) -> DailySleepModel: @@ -45,5 +50,7 @@ def get_daily_sleep_document(self, document_id: str) -> DailySleepModel: Returns: DailySleepModel: Response containing daily sleep data. """ - response = self.client._make_request(f"/v2/usercollection/daily_sleep/{document_id}") + response = self.client._make_request( + f"/v2/usercollection/daily_sleep/{document_id}" + ) return DailySleepModel(**response) diff --git a/oura_api_client/api/daily_spo2.py b/oura_api_client/api/daily_spo2.py index 2da2a3e..7ef56a7 100644 --- a/oura_api_client/api/daily_spo2.py +++ b/oura_api_client/api/daily_spo2.py @@ -1,7 +1,10 @@ from typing import Optional, Union from datetime import date from oura_api_client.api.base import BaseRouter -from oura_api_client.models.daily_spo2 import DailySpO2Response, DailySpO2Model +from oura_api_client.models.daily_spo2 import ( + DailySpO2Response, + DailySpO2Model +) class DailySpo2(BaseRouter): # Renamed class to DailySpo2 @@ -32,10 +35,14 @@ def get_daily_spo2_documents( # Renamed method "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/daily_spo2", params=params) + response = self.client._make_request( + "/v2/usercollection/daily_spo2", params=params + ) return DailySpO2Response(**response) - def get_daily_spo2_document(self, document_id: str) -> DailySpO2Model: # Renamed method and updated return type + def get_daily_spo2_document( + self, document_id: str + ) -> DailySpO2Model: # Renamed method and updated return type """ Get a single daily SpO2 document. @@ -45,5 +52,7 @@ def get_daily_spo2_document(self, document_id: str) -> DailySpO2Model: # Rename Returns: DailySpO2Model: Response containing daily SpO2 data. """ - response = self.client._make_request(f"/v2/usercollection/daily_spo2/{document_id}") + response = self.client._make_request( + f"/v2/usercollection/daily_spo2/{document_id}" + ) return DailySpO2Model(**response) diff --git a/oura_api_client/api/daily_stress.py b/oura_api_client/api/daily_stress.py index 81b8be7..b8d0d3b 100644 --- a/oura_api_client/api/daily_stress.py +++ b/oura_api_client/api/daily_stress.py @@ -1,7 +1,10 @@ from typing import Optional, Union from datetime import date from oura_api_client.api.base import BaseRouter -from oura_api_client.models.daily_stress import DailyStressResponse, DailyStressModel +from oura_api_client.models.daily_stress import ( + DailyStressResponse, + DailyStressModel +) class DailyStress(BaseRouter): @@ -32,7 +35,9 @@ def get_daily_stress_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/daily_stress", params=params) + response = self.client._make_request( + "/v2/usercollection/daily_stress", params=params + ) return DailyStressResponse(**response) def get_daily_stress_document(self, document_id: str) -> DailyStressModel: @@ -45,5 +50,7 @@ def get_daily_stress_document(self, document_id: str) -> DailyStressModel: Returns: DailyStressModel: Response containing daily stress data. """ - response = self.client._make_request(f"/v2/usercollection/daily_stress/{document_id}") + response = self.client._make_request( + f"/v2/usercollection/daily_stress/{document_id}" + ) return DailyStressModel(**response) diff --git a/oura_api_client/api/enhanced_tag.py b/oura_api_client/api/enhanced_tag.py index 7bb713a..ffcfea9 100644 --- a/oura_api_client/api/enhanced_tag.py +++ b/oura_api_client/api/enhanced_tag.py @@ -1,7 +1,10 @@ from typing import Optional, Union from datetime import date # Using date for start_date and end_date from oura_api_client.api.base import BaseRouter -from oura_api_client.models.enhanced_tag import EnhancedTagResponse, EnhancedTagModel +from oura_api_client.models.enhanced_tag import ( + EnhancedTagResponse, + EnhancedTagModel +) class EnhancedTag(BaseRouter): @@ -32,7 +35,9 @@ def get_enhanced_tag_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/enhanced_tag", params=params) + response = self.client._make_request( + "/v2/usercollection/enhanced_tag", params=params + ) return EnhancedTagResponse(**response) def get_enhanced_tag_document(self, document_id: str) -> EnhancedTagModel: @@ -45,5 +50,7 @@ def get_enhanced_tag_document(self, document_id: str) -> EnhancedTagModel: Returns: EnhancedTagModel: Response containing enhanced_tag data. """ - response = self.client._make_request(f"/v2/usercollection/enhanced_tag/{document_id}") + response = self.client._make_request( + f"/v2/usercollection/enhanced_tag/{document_id}" + ) return EnhancedTagModel(**response) diff --git a/oura_api_client/api/heartrate.py b/oura_api_client/api/heartrate.py index 41ba47c..1c74edb 100644 --- a/oura_api_client/api/heartrate.py +++ b/oura_api_client/api/heartrate.py @@ -38,7 +38,9 @@ def get_heartrate( if end_date: params["end_date"] = end_date - response = self.client._make_request("/usercollection/heartrate", params=params) + response = self.client._make_request( + "/usercollection/heartrate", params=params + ) if return_model: return HeartRateResponse.from_dict(response) diff --git a/oura_api_client/api/rest_mode_period.py b/oura_api_client/api/rest_mode_period.py index 3b14a0a..695f62c 100644 --- a/oura_api_client/api/rest_mode_period.py +++ b/oura_api_client/api/rest_mode_period.py @@ -1,7 +1,10 @@ from typing import Optional, Union from datetime import date from oura_api_client.api.base import BaseRouter -from oura_api_client.models.rest_mode_period import RestModePeriodResponse, RestModePeriodModel +from oura_api_client.models.rest_mode_period import ( + RestModePeriodResponse, + RestModePeriodModel +) class RestModePeriod(BaseRouter): @@ -32,10 +35,14 @@ def get_rest_mode_period_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/rest_mode_period", params=params) + response = self.client._make_request( + "/v2/usercollection/rest_mode_period", params=params + ) return RestModePeriodResponse(**response) - def get_rest_mode_period_document(self, document_id: str) -> RestModePeriodModel: + def get_rest_mode_period_document( + self, document_id: str + ) -> RestModePeriodModel: """ Get a single rest_mode_period document. @@ -45,5 +52,7 @@ def get_rest_mode_period_document(self, document_id: str) -> RestModePeriodModel Returns: RestModePeriodModel: Response containing rest_mode_period data. """ - response = self.client._make_request(f"/v2/usercollection/rest_mode_period/{document_id}") + response = self.client._make_request( + f"/v2/usercollection/rest_mode_period/{document_id}" + ) return RestModePeriodModel(**response) diff --git a/oura_api_client/api/ring_configuration.py b/oura_api_client/api/ring_configuration.py index 0b24bf0..434846f 100644 --- a/oura_api_client/api/ring_configuration.py +++ b/oura_api_client/api/ring_configuration.py @@ -1,7 +1,10 @@ from typing import Optional, Union # Union is not strictly needed here but kept for consistency from datetime import date # date is not used by ring_configuration but kept for consistency from oura_api_client.api.base import BaseRouter -from oura_api_client.models.ring_configuration import RingConfigurationResponse, RingConfigurationModel +from oura_api_client.models.ring_configuration import ( + RingConfigurationResponse, + RingConfigurationModel +) class RingConfiguration(BaseRouter): @@ -11,7 +14,7 @@ def get_ring_configuration_documents( # as it often returns a single current configuration or a list of all historical ones. # However, if the API supports it (e.g. for historical configurations): start_date: Optional[Union[str, date]] = None, # Kept for potential future use or specific API design - end_date: Optional[Union[str, date]] = None, # Kept for potential future use + end_date: Optional[Union[str, date]] = None, # Kept for potential future use next_token: Optional[str] = None, ) -> RingConfigurationResponse: """ @@ -45,10 +48,15 @@ def get_ring_configuration_documents( # Remove None params manually as empty dict evaluates to False final_params = {k: v for k, v in params.items() if v is not None} - response = self.client._make_request("/v2/usercollection/ring_configuration", params=final_params if final_params else None) + response = self.client._make_request( + "/v2/usercollection/ring_configuration", + params=final_params if final_params else None + ) return RingConfigurationResponse(**response) - def get_ring_configuration_document(self, document_id: str) -> RingConfigurationModel: + def get_ring_configuration_document( + self, document_id: str + ) -> RingConfigurationModel: """ Get a single ring configuration document. @@ -58,5 +66,7 @@ def get_ring_configuration_document(self, document_id: str) -> RingConfiguration Returns: RingConfigurationModel: Response containing ring configuration data. """ - response = self.client._make_request(f"/v2/usercollection/ring_configuration/{document_id}") + response = self.client._make_request( + f"/v2/usercollection/ring_configuration/{document_id}" + ) return RingConfigurationModel(**response) diff --git a/oura_api_client/api/session.py b/oura_api_client/api/session.py index 1b7ade6..3245128 100644 --- a/oura_api_client/api/session.py +++ b/oura_api_client/api/session.py @@ -1,5 +1,6 @@ from typing import Optional, Union -from datetime import date # Using date for start_date and end_date as per other endpoints +from datetime import date # Using date for start_date and end_date +# as per other endpoints from oura_api_client.api.base import BaseRouter from oura_api_client.models.session import SessionResponse, SessionModel @@ -7,8 +8,10 @@ class Session(BaseRouter): def get_session_documents( self, - start_date: Optional[Union[str, date]] = None, # Changed from start_datetime for consistency - end_date: Optional[Union[str, date]] = None, # Changed from end_datetime for consistency + start_date: Optional[Union[str, date]] = None, # Changed from + # start_datetime for consistency + end_date: Optional[Union[str, date]] = None, # Changed from + # end_datetime for consistency next_token: Optional[str] = None, ) -> SessionResponse: """ @@ -32,7 +35,9 @@ def get_session_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/session", params=params) + response = self.client._make_request( + "/v2/usercollection/session", params=params + ) return SessionResponse(**response) def get_session_document(self, document_id: str) -> SessionModel: @@ -45,5 +50,7 @@ def get_session_document(self, document_id: str) -> SessionModel: Returns: SessionModel: Response containing session data. """ - response = self.client._make_request(f"/v2/usercollection/session/{document_id}") + response = self.client._make_request( + f"/v2/usercollection/session/{document_id}" + ) return SessionModel(**response) diff --git a/oura_api_client/api/sleep.py b/oura_api_client/api/sleep.py index f901465..7b24d8a 100644 --- a/oura_api_client/api/sleep.py +++ b/oura_api_client/api/sleep.py @@ -1,14 +1,19 @@ from typing import Optional, Union from datetime import date # Keep date for start/end_date from oura_api_client.api.base import BaseRouter -from oura_api_client.models.sleep import SleepResponse, SleepModel # Updated model import +from oura_api_client.models.sleep import ( + SleepResponse, + SleepModel # Updated model import +) class Sleep(BaseRouter): # Renamed class to Sleep def get_sleep_documents( # Renamed method self, - start_date: Optional[Union[str, date]] = None, # Changed parameter name for clarity - end_date: Optional[Union[str, date]] = None, # Changed parameter name for clarity + start_date: Optional[Union[str, date]] = None, # Changed + # parameter name for clarity + end_date: Optional[Union[str, date]] = None, # Changed + # parameter name for clarity next_token: Optional[str] = None, ) -> SleepResponse: # Updated return type """ @@ -33,10 +38,14 @@ def get_sleep_documents( # Renamed method } params = {k: v for k, v in params.items() if v is not None} # Corrected endpoint URL from daily_sleep to sleep - response = self.client._make_request("/v2/usercollection/sleep", params=params) + response = self.client._make_request( + "/v2/usercollection/sleep", params=params + ) return SleepResponse(**response) - def get_sleep_document(self, document_id: str) -> SleepModel: # Renamed method and updated return type + def get_sleep_document( + self, document_id: str + ) -> SleepModel: # Renamed method and updated return type """ Get a single sleep document. @@ -47,5 +56,7 @@ def get_sleep_document(self, document_id: str) -> SleepModel: # Renamed method SleepModel: Response containing sleep data. """ # Corrected endpoint URL from daily_sleep to sleep - response = self.client._make_request(f"/v2/usercollection/sleep/{document_id}") + response = self.client._make_request( + f"/v2/usercollection/sleep/{document_id}" + ) return SleepModel(**response) diff --git a/oura_api_client/api/sleep_time.py b/oura_api_client/api/sleep_time.py index 879da8d..090132f 100644 --- a/oura_api_client/api/sleep_time.py +++ b/oura_api_client/api/sleep_time.py @@ -1,7 +1,10 @@ from typing import Optional, Union from datetime import date from oura_api_client.api.base import BaseRouter -from oura_api_client.models.sleep_time import SleepTimeResponse, SleepTimeModel +from oura_api_client.models.sleep_time import ( + SleepTimeResponse, + SleepTimeModel +) class SleepTime(BaseRouter): @@ -32,7 +35,9 @@ def get_sleep_time_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/sleep_time", params=params) + response = self.client._make_request( + "/v2/usercollection/sleep_time", params=params + ) return SleepTimeResponse(**response) def get_sleep_time_document(self, document_id: str) -> SleepTimeModel: @@ -49,7 +54,10 @@ def get_sleep_time_document(self, document_id: str) -> SleepTimeModel: Returns: SleepTimeModel: Response containing sleep time data. """ - # This endpoint might not be available in Oura API v2 for sleep_time. - # Proceeding with the assumption it might exist or for future compatibility. - response = self.client._make_request(f"/v2/usercollection/sleep_time/{document_id}") + # This endpoint might not be available in Oura API v2 for + # sleep_time. Proceeding with the assumption it might exist or + # for future compatibility. + response = self.client._make_request( + f"/v2/usercollection/sleep_time/{document_id}" + ) return SleepTimeModel(**response) diff --git a/oura_api_client/api/tag.py b/oura_api_client/api/tag.py index 29115a6..bf75cc8 100644 --- a/oura_api_client/api/tag.py +++ b/oura_api_client/api/tag.py @@ -32,7 +32,9 @@ def get_tag_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/tag", params=params) + response = self.client._make_request( + "/v2/usercollection/tag", params=params + ) return TagResponse(**response) def get_tag_document(self, document_id: str) -> TagModel: @@ -45,5 +47,7 @@ def get_tag_document(self, document_id: str) -> TagModel: Returns: TagModel: Response containing tag data. """ - response = self.client._make_request(f"/v2/usercollection/tag/{document_id}") + response = self.client._make_request( + f"/v2/usercollection/tag/{document_id}" + ) return TagModel(**response) diff --git a/oura_api_client/api/vo2_max.py b/oura_api_client/api/vo2_max.py index f6d6aba..19b3afd 100644 --- a/oura_api_client/api/vo2_max.py +++ b/oura_api_client/api/vo2_max.py @@ -32,7 +32,9 @@ 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: @@ -45,5 +47,7 @@ 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) diff --git a/oura_api_client/api/webhook.py b/oura_api_client/api/webhook.py index 3fd2cb8..96551a8 100644 --- a/oura_api_client/api/webhook.py +++ b/oura_api_client/api/webhook.py @@ -13,23 +13,30 @@ 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'): - # 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.") + 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. + # 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. + # 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]: @@ -50,14 +57,16 @@ def create_webhook_subscription( callback_url: str, event_type: WebhookOperation, data_type: ExtApiV2DataType, - verification_token: str, # Made non-optional as per updated model reflecting spec + verification_token: str, # Made non-optional as per updated + # model reflecting spec ) -> WebhookSubscriptionModel: """ Create a new webhook subscription. 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 + # 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' @@ -70,12 +79,16 @@ def create_webhook_subscription( response_data = self.client._make_request( "/v2/webhook/subscription", method="POST", - json_data=request_body.model_dump(by_alias=True), # exclude_none=True is default for model_dump + json_data=request_body.model_dump( + by_alias=True + ), # exclude_none=True is default for model_dump headers=headers ) return WebhookSubscriptionModel(**response_data) - def get_webhook_subscription(self, subscription_id: str) -> WebhookSubscriptionModel: + def get_webhook_subscription( + self, subscription_id: str + ) -> WebhookSubscriptionModel: """ Get details for a specific webhook subscription. API Path: GET /v2/webhook/subscription/{subscription_id} @@ -90,7 +103,7 @@ def get_webhook_subscription(self, subscription_id: str) -> WebhookSubscriptionM def update_webhook_subscription( self, subscription_id: str, - verification_token: str, # Required + verification_token: str, # Required callback_url: Optional[str] = None, event_type: Optional[WebhookOperation] = None, data_type: Optional[ExtApiV2DataType] = None, @@ -101,10 +114,10 @@ def update_webhook_subscription( """ headers = self._get_webhook_headers() if 'Content-Type' not in headers: - headers['Content-Type'] = 'application/json' + headers['Content-Type'] = 'application/json' request_body = WebhookSubscriptionUpdateRequest( - verification_token=verification_token, # Now required + verification_token=verification_token, # Now required callback_url=callback_url, event_type=event_type, data_type=data_type, @@ -112,7 +125,9 @@ def update_webhook_subscription( 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_data) @@ -130,15 +145,17 @@ def delete_webhook_subscription(self, subscription_id: str) -> None: ) return None - def renew_webhook_subscription(self, subscription_id: str) -> WebhookSubscriptionModel: + 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. + # 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' @@ -146,7 +163,8 @@ def renew_webhook_subscription(self, subscription_id: str) -> WebhookSubscriptio 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. + # 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) diff --git a/oura_api_client/api/workout.py b/oura_api_client/api/workout.py index 4f63983..590315f 100644 --- a/oura_api_client/api/workout.py +++ b/oura_api_client/api/workout.py @@ -32,7 +32,9 @@ def get_workout_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/workout", params=params) + response = self.client._make_request( + "/v2/usercollection/workout", params=params + ) return WorkoutResponse(**response) def get_workout_document(self, document_id: str) -> WorkoutModel: @@ -45,5 +47,7 @@ def get_workout_document(self, document_id: str) -> WorkoutModel: Returns: WorkoutModel: Response containing workout data. """ - response = self.client._make_request(f"/v2/usercollection/workout/{document_id}") + response = self.client._make_request( + f"/v2/usercollection/workout/{document_id}" + ) return WorkoutModel(**response) diff --git a/oura_api_client/models/daily_activity.py b/oura_api_client/models/daily_activity.py index 78e8a1a..5d2b521 100644 --- a/oura_api_client/models/daily_activity.py +++ b/oura_api_client/models/daily_activity.py @@ -17,21 +17,37 @@ class DailyActivityModel(BaseModel): class_5_min: Optional[str] = Field(None, alias="class_5_min") score: Optional[int] = Field(None, alias="score") active_calories: Optional[int] = Field(None, alias="active_calories") - average_met_minutes: Optional[float] = Field(None, alias="average_met_minutes") - contributors: Optional[ActivityContributors] = Field(None, alias="contributors") - equivalent_walking_distance: Optional[int] = Field(None, alias="equivalent_walking_distance") - high_activity_met_minutes: Optional[int] = Field(None, alias="high_activity_met_minutes") + average_met_minutes: Optional[float] = Field( + None, alias="average_met_minutes" + ) + contributors: Optional[ActivityContributors] = Field( + None, alias="contributors" + ) + equivalent_walking_distance: Optional[int] = Field( + None, alias="equivalent_walking_distance" + ) + high_activity_met_minutes: Optional[int] = Field( + None, alias="high_activity_met_minutes" + ) high_activity_time: Optional[int] = Field(None, alias="high_activity_time") inactivity_alerts: Optional[int] = Field(None, alias="inactivity_alerts") - low_activity_met_minutes: Optional[int] = Field(None, alias="low_activity_met_minutes") + low_activity_met_minutes: Optional[int] = Field( + None, alias="low_activity_met_minutes" + ) low_activity_time: Optional[int] = Field(None, alias="low_activity_time") - medium_activity_met_minutes: Optional[int] = Field(None, alias="medium_activity_met_minutes") - medium_activity_time: Optional[int] = Field(None, alias="medium_activity_time") + medium_activity_met_minutes: Optional[int] = Field( + None, alias="medium_activity_met_minutes" + ) + medium_activity_time: Optional[int] = Field( + None, alias="medium_activity_time" + ) met: Optional[str] = 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") - sedentary_met_minutes: Optional[int] = Field(None, alias="sedentary_met_minutes") + sedentary_met_minutes: Optional[int] = Field( + None, alias="sedentary_met_minutes" + ) sedentary_time: Optional[int] = Field(None, alias="sedentary_time") steps: Optional[int] = Field(None, alias="steps") target_calories: Optional[int] = Field(None, alias="target_calories") diff --git a/oura_api_client/models/daily_cardiovascular_age.py b/oura_api_client/models/daily_cardiovascular_age.py index 1185005..dfd4dee 100644 --- a/oura_api_client/models/daily_cardiovascular_age.py +++ b/oura_api_client/models/daily_cardiovascular_age.py @@ -6,17 +6,23 @@ class DailyCardiovascularAgeModel(BaseModel): id: str day: date - # Based on typical cardiovascular age report from Oura: - cardiovascular_age: Optional[int] = None # Age in years - age_difference: Optional[int] = None # Difference from chronological age - age_upper_bound: Optional[float] = Field(None, alias="age_upper_bound") # Upper bound of the estimated age range - # Other potential fields, depending on API detail: - # arterial_stiffness_index: Optional[float] = Field(None, alias="arterial_stiffness_index") - confidence: Optional[float] = None # Confidence score - # pulse_wave_velocity: Optional[float] = Field(None, alias="pulse_wave_velocity") + # Based on OpenAPI spec: + vascular_age: Optional[float] = Field( + None, alias="vascular_age" + ) # The user's estimated vascular age + cardiovascular_age: Optional[float] = Field( + None, alias="cardiovascular_age" + ) # The user's estimated cardiovascular age + age_lower_bound: Optional[float] = Field( + None, alias="age_lower_bound" + ) # Lower bound of the estimated age range + age_upper_bound: Optional[float] = Field( + None, alias="age_upper_bound" + ) # Upper bound of the estimated age range timestamp: datetime # Timestamp of the summary class DailyCardiovascularAgeResponse(BaseModel): data: List[DailyCardiovascularAgeModel] + next_token: Optional[str] = None # Pagination token source: Optional[str] = None # Data source diff --git a/oura_api_client/models/daily_readiness.py b/oura_api_client/models/daily_readiness.py index 81f5801..4eef602 100644 --- a/oura_api_client/models/daily_readiness.py +++ b/oura_api_client/models/daily_readiness.py @@ -1,5 +1,5 @@ from pydantic import BaseModel, Field -from typing import List, Optional +from typing import List, Optional # Added List from datetime import date, datetime @@ -7,10 +7,14 @@ class ReadinessContributors(BaseModel): activity_balance: Optional[int] = Field(None, alias="activity_balance") body_temperature: Optional[int] = Field(None, alias="body_temperature") hrv_balance: Optional[int] = Field(None, alias="hrv_balance") - previous_day_activity: Optional[int] = Field(None, alias="previous_day_activity") + previous_day_activity: Optional[int] = Field( + None, alias="previous_day_activity" + ) previous_night: Optional[int] = Field(None, alias="previous_night") recovery_index: Optional[int] = Field(None, alias="recovery_index") - resting_heart_rate: Optional[int] = Field(None, alias="resting_heart_rate") + resting_heart_rate: Optional[int] = Field( + None, alias="resting_heart_rate" + ) sleep_balance: Optional[int] = Field(None, alias="sleep_balance") @@ -19,18 +23,35 @@ class DailyReadinessModel(BaseModel): contributors: ReadinessContributors day: date score: Optional[int] = Field(None, alias="score") - temperature_deviation: Optional[float] = Field(None, alias="temperature_deviation") # Deprecated - temperature_trend_deviation: Optional[float] = Field(None, alias="temperature_trend_deviation") + temperature_deviation: Optional[float] = Field( + None, alias="temperature_deviation" + ) # Deprecated + temperature_trend_deviation: Optional[float] = Field( + None, alias="temperature_trend_deviation" + ) timestamp: datetime # New fields from OpenAPI spec not in original snippet - activity_class_5_min: Optional[str] = Field(None, alias="activity_class_5_min") # New - hrv_balance_data: Optional[str] = Field(None, alias="hrv_balance_data") # New, assuming string, adjust if different type - spo2_percentage: Optional[float] = Field(None, alias="spo2_percentage") # New - # Fields from original DailyActivity/Sleep that might be relevant or were missed in initial Readiness scope - # Assuming these are not part of readiness based on typical Oura data separation, - # but including as comments if they need to be reviewed from a more comprehensive spec - # sleep_average: Optional[int] = Field(None, alias="sleep_average") # Example if there was a sleep_average field - # readiness_score_delta: Optional[int] = Field(None, alias="readiness_score_delta") # This was in sleep, likely not here + activity_class_5_min: Optional[str] = Field( + None, alias="activity_class_5_min" + ) # New + hrv_balance_data: Optional[str] = Field( + None, alias="hrv_balance_data" + ) # New, assuming string, adjust if different type + spo2_percentage: Optional[float] = Field( + None, alias="spo2_percentage" + ) # New + # Fields from original DailyActivity/Sleep that might be relevant or + # were missed in initial Readiness scope + # Assuming these are not part of readiness based on typical Oura data + # separation, + # but including as comments if they need to be reviewed from a more + # comprehensive spec + # sleep_average: Optional[int] = Field( + # None, alias="sleep_average" + # ) # Example if there was a sleep_average field + # readiness_score_delta: Optional[int] = Field( + # None, alias="readiness_score_delta" + # ) # This was in sleep, likely not here class DailyReadinessResponse(BaseModel): diff --git a/oura_api_client/models/daily_resilience.py b/oura_api_client/models/daily_resilience.py index ebc1e25..27dd8d6 100644 --- a/oura_api_client/models/daily_resilience.py +++ b/oura_api_client/models/daily_resilience.py @@ -4,15 +4,30 @@ from datetime import date, datetime +class ResilienceContributors(BaseModel): + sleep_recovery: Optional[float] = Field( + None, alias="sleep_recovery" + ) + daytime_recovery: Optional[float] = Field( + None, alias="daytime_recovery" + ) + stress: Optional[float] = Field( + None, alias="stress" + ) + + class DailyResilienceModel(BaseModel): id: str day: date - resilience_score: Optional[float] = Field(None, alias="resilience_score") # Overall resilience score - # Based on typical resilience metrics, fields could include: - # stress_regulation: Optional[float] = Field(None, alias="stress_regulation") - # recovery_patterns: Optional[float] = Field(None, alias="recovery_patterns") - # emotional_balance: Optional[float] = Field(None, alias="emotional_balance") - # These are examples; actual fields depend on the Oura API's definition of resilience. + resilience_score: Optional[float] = Field( + None, alias="resilience_score" + ) # Overall resilience score + contributors: Optional[ResilienceContributors] = Field( + None, alias="contributors" + ) # Resilience contributors + level: Optional[str] = Field( + None, alias="level" + ) # Resilience level timestamp: datetime # Timestamp of the summary diff --git a/oura_api_client/models/daily_sleep.py b/oura_api_client/models/daily_sleep.py index 084a9eb..ed274b9 100644 --- a/oura_api_client/models/daily_sleep.py +++ b/oura_api_client/models/daily_sleep.py @@ -4,8 +4,11 @@ from datetime import date, datetime + class SleepContributors(BaseModel): - deep_sleep: Optional[int] = Field(None, alias="deep_sleep") # deep sleep in minutes + deep_sleep: Optional[int] = Field( + None, alias="deep_sleep" + ) efficiency: Optional[int] = Field(None, alias="efficiency") latency: Optional[int] = Field(None, alias="latency") rem_sleep: Optional[int] = Field(None, alias="rem_sleep") # REM sleep in minutes @@ -14,38 +17,112 @@ class SleepContributors(BaseModel): total_sleep: Optional[int] = Field(None, alias="total_sleep") # Total sleep in minutes + + + + class DailySleepModel(BaseModel): id: str contributors: SleepContributors day: date timestamp: datetime - score: Optional[int] = Field(None, alias="score") + score: Optional[int] = Field( + + + + + None, alias="score" + + + + +) bedtime_end: Optional[datetime] = Field(None, alias="bedtime_end") bedtime_start: Optional[datetime] = Field(None, alias="bedtime_start") breath_average: Optional[float] = Field(None, alias="breath_average") - deep_sleep_duration: Optional[int] = Field(None, alias="deep_sleep_duration") + deep_sleep_duration: Optional[int] = Field( + + None, alias="deep_sleep_duration" + + ) efficiency: Optional[int] = Field(None, alias="efficiency") - heart_rate_average: Optional[float] = Field(None, alias="heart_rate_average") - heart_rate_lowest: Optional[float] = Field(None, alias="heart_rate_lowest") + heart_rate_average: Optional[float] = Field( + + None, alias="heart_rate_average" + + ) + heart_rate_lowest: Optional[float] = Field( + + None, alias="heart_rate_lowest" + + ) hypnogram_5_min: Optional[str] = Field(None, alias="hypnogram_5_min") latency: Optional[int] = Field(None, alias="latency") - light_sleep_duration: Optional[int] = Field(None, alias="light_sleep_duration") + light_sleep_duration: Optional[int] = Field( + + None, alias="light_sleep_duration" + + ) low_battery_alert: Optional[bool] = Field(None, alias="low_battery_alert") - readiness_score_delta: Optional[int] = Field(None, alias="readiness_score_delta") - rem_sleep_duration: Optional[int] = Field(None, alias="rem_sleep_duration") + readiness_score_delta: Optional[int] = Field( + + None, alias="readiness_score_delta" + + ) + rem_sleep_duration: Optional[int] = Field( + + None, alias="rem_sleep_duration" + + ) restless_periods: Optional[int] = Field(None, alias="restless_periods") - sleep_phase_5_min: Optional[str] = Field(None, alias="sleep_phase_5_min") # Deprecated + sleep_phase_5_min: Optional[str] = Field( + + None, alias="sleep_phase_5_min" + + ) # Deprecated time_in_bed: Optional[int] = Field(None, alias="time_in_bed") - total_sleep_duration: Optional[int] = Field(None, alias="total_sleep_duration") - type: Optional[str] = Field(None, alias="type") # Enum: "deleted", "long_sleep", "main_sleep", "nap", "rest" + total_sleep_duration: Optional[int] = Field( + + None, alias="total_sleep_duration" + + ) + type: Optional[str] = Field( + + None, alias="type" + + ) # Enum: "deleted", "long_sleep", "main_sleep", "nap", "rest" average_hrv: Optional[float] = Field(None, alias="average_hrv") awake_time: Optional[int] = Field(None, alias="awake_time") - hr_60_second_average: Optional[List[int]] = Field(None, alias="hr_60_second_average") # New in v2.10 - hrv_4_hour_average: Optional[List[float]] = Field(None, alias="hrv_4_hour_average") # New in v2.10 - readiness: Optional[str] = Field(None, alias="readiness") # New in v2.10, but type not specified, assuming string for now - temperature_delta: Optional[float] = Field(None, alias="temperature_delta") - temperature_deviation: Optional[float] = Field(None, alias="temperature_deviation") # Deprecated - temperature_trend_deviation: Optional[float] = Field(None, alias="temperature_trend_deviation") + hr_60_second_average: Optional[List[int]] = Field( + + None, alias="hr_60_second_average" + + ) # New in v2.10 + hrv_4_hour_average: Optional[List[float]] = Field( + + None, alias="hrv_4_hour_average" + + ) # New in v2.10 + readiness: Optional[str] = Field( + + None, alias="readiness" + + ) # New in v2.10, but type not specified, assuming string for now + temperature_delta: Optional[float] = Field( + + None, alias="temperature_delta" + + ) + temperature_deviation: Optional[float] = Field( + + None, alias="temperature_deviation" + + ) # Deprecated + temperature_trend_deviation: Optional[float] = Field( + + None, alias="temperature_trend_deviation" + + ) class DailySleepResponse(BaseModel): diff --git a/oura_api_client/models/daily_spo2.py b/oura_api_client/models/daily_spo2.py index ea06de4..213e6ea 100644 --- a/oura_api_client/models/daily_spo2.py +++ b/oura_api_client/models/daily_spo2.py @@ -1,21 +1,39 @@ from pydantic import BaseModel, Field from typing import List, Optional -from datetime import date, datetime +from datetime import date, datetime # Added datetime class DailySpO2AggregatedValuesModel(BaseModel): # Renamed from Spo2Readings to DailySpO2AggregatedValuesModel for clarity - average: Optional[float] = Field(None, alias="average") # Percentage + average: Optional[float] = Field( + None, alias="average" + ) # Percentage class DailySpO2Model(BaseModel): id: str day: date - spo2_percentage: Optional[float] = None # Overall percentage for the day, if available + spo2_percentage: Optional[float] = Field( + + + + None, alias="spo2_percentage" + + + +) # Overall percentage for the day, if available # The above field seems redundant if aggregated_values.average is the main source # Kept for now as per some interpretations of daily summary vs. detailed readings - aggregated_values: Optional[DailySpO2AggregatedValuesModel] = Field(None, alias="aggregated_values") # New nested model for clarity + aggregated_values: Optional[DailySpO2AggregatedValuesModel] = Field( + + None, alias="aggregated_values" + + ) # New nested model for clarity # Assuming timestamp might be relevant for when the daily record was created or last updated - timestamp: Optional[datetime] = Field(None, alias="timestamp") # Added timestamp + timestamp: Optional[datetime] = Field( + + None, alias="timestamp" + + ) # Added timestamp class DailySpO2Response(BaseModel): diff --git a/oura_api_client/models/daily_stress.py b/oura_api_client/models/daily_stress.py index e5b7a48..e1abe69 100644 --- a/oura_api_client/models/daily_stress.py +++ b/oura_api_client/models/daily_stress.py @@ -6,17 +6,29 @@ class DailyStressModel(BaseModel): id: str day: date - stress_high: Optional[int] = Field(None, alias="stress_high") # Duration of high stress in seconds - stress_score: Optional[int] = None # Stress score - stress_medium: Optional[int] = Field(None, alias="stress_medium") # Duration of medium stress in seconds + stress_high: Optional[int] = Field( + None, alias="stress_high" + ) # Duration of high stress in seconds + stress_low: Optional[int] = Field( + None, alias="stress_low" + ) # Duration of low stress in seconds + stress_medium: Optional[int] = Field( + None, alias="stress_medium" + ) # Duration of medium stress in seconds timestamp: datetime # Timestamp of the summary - # Based on OpenAPI spec, additional fields might include: - # recovery_high: Optional[int] = Field(None, alias="recovery_high") # Duration of high recovery in seconds - # recovery_low: Optional[int] = Field(None, alias="recovery_low") # Duration of low recovery in seconds - # recovery_medium: Optional[int] = Field(None, alias="recovery_medium") # Duration of medium recovery in seconds - # daytime_stress_score: Optional[int] = Field(None, alias="daytime_stress_score") # Overall stress score - # daytime_stress_score: Optional[int] = Field(None, alias="daytime_stress_score") # Overall stress score - # Deprecated fields like `rest_mode_state` are not included unless specified as current. + # Based on OpenAPI spec, additional fields: + recovery_high: Optional[int] = Field( + None, alias="recovery_high" + ) # Duration of high recovery in seconds + recovery_low: Optional[int] = Field( + None, alias="recovery_low" + ) # Duration of low recovery in seconds + recovery_medium: Optional[int] = Field( + None, alias="recovery_medium" + ) # Duration of medium recovery in seconds + day_summary: Optional[str] = Field( + None, alias="day_summary" + ) # Summary of the day's stress class DailyStressResponse(BaseModel): diff --git a/oura_api_client/models/enhanced_tag.py b/oura_api_client/models/enhanced_tag.py index 793c85a..ce1beb9 100644 --- a/oura_api_client/models/enhanced_tag.py +++ b/oura_api_client/models/enhanced_tag.py @@ -5,12 +5,23 @@ class EnhancedTagModel(BaseModel): id: str - tag_type_code: str # e.g., "common_cold", "period" + tag_type_code: str = Field( + alias="tag_type_code" + ) # e.g., "common_cold", "period" start_time: datetime = Field(alias="start_time") end_time: Optional[datetime] = Field(None, alias="end_time") - start_day: Optional[date] = Field(None, alias="start_day") # New based on typical usage - end_day: Optional[date] = Field(None, alias="end_day") # New based on typical usage - comment: Optional[str] = None # Based on OpenAPI spec, there might be other fields, + start_day: Optional[date] = Field( + + None, alias="start_day" + + ) # New based on typical usage + end_day: Optional[date] = Field( + + None, alias="end_day" + + ) # New based on typical usage + comment: Optional[str] = None + # Based on OpenAPI spec, there might be other fields, # but these are the core ones usually associated with enhanced tags. # If a more detailed spec is available, other fields like 'icon' or 'source' could be added. diff --git a/oura_api_client/models/heartrate.py b/oura_api_client/models/heartrate.py index b17259e..a1c379f 100644 --- a/oura_api_client/models/heartrate.py +++ b/oura_api_client/models/heartrate.py @@ -6,6 +6,7 @@ @dataclass + class HeartRateSample: """Represents a single heart rate data point.""" @@ -31,6 +32,7 @@ def from_dict(cls, data: dict) -> "HeartRateSample": @dataclass + class HeartRateResponse: """Represents the full heart rate response.""" diff --git a/oura_api_client/models/personal.py b/oura_api_client/models/personal.py index 183978f..3ca6817 100644 --- a/oura_api_client/models/personal.py +++ b/oura_api_client/models/personal.py @@ -6,6 +6,7 @@ @dataclass + class PersonalInfo: """Represents personal information for a user.""" diff --git a/oura_api_client/models/rest_mode_period.py b/oura_api_client/models/rest_mode_period.py index 989edfd..33b8ac6 100644 --- a/oura_api_client/models/rest_mode_period.py +++ b/oura_api_client/models/rest_mode_period.py @@ -40,17 +40,34 @@ class RestModeEpisode(BaseModel): class RestModePeriodModel(BaseModel): id: str day: date - start_time: datetime # Timestamp of the summary - end_time: Optional[datetime] = Field(None, alias="end_time") + start_time: datetime = Field( + alias="start_time" + ) + end_time: Optional[datetime] = Field( + None, alias="end_time" + ) # Rest mode specific state or tag, e.g. "on_demand_rest", "recovering_from_illness" - rest_mode_state: Optional[str] = Field(None, alias="rest_mode_state") # Example: "on_demand_rest" + rest_mode_state: Optional[str] = Field( + None, alias="rest_mode_state" + ) # Example: "on_demand_rest" # If RestModeEpisode was a list of sub-items: - # episodes: Optional[List[RestModeEpisode]] = Field(None, alias="episodes") + # episodes: Optional[List[RestModeEpisode]] = Field( + # None, alias="episodes" + # ) # However, the OpenAPI spec has a flat structure for RestModePeriodModel. # Adding fields from OpenAPI spec for RestModePeriod - baseline_heart_rate: Optional[int] = Field(None, alias="baseline_heart_rate") + baseline_heart_rate: Optional[int] = Field( + None, alias="baseline_heart_rate" + ) baseline_hrv: Optional[int] = Field(None, alias="baseline_hrv") - baseline_skin_temperature: Optional[float] = Field(None, alias="baseline_skin_temperature") + baseline_skin_temperature: Optional[float] = Field( + None, alias="baseline_skin_temperature" + ) + # 'day' is already included + # 'end_time' is already included + # 'id' is already included + # 'rest_mode_state' is already included (as 'state' in some contexts, but using rest_mode_state for clarity) + # 'start_time' is already included class RestModePeriodResponse(BaseModel): diff --git a/oura_api_client/models/ring_configuration.py b/oura_api_client/models/ring_configuration.py index c69d034..c888102 100644 --- a/oura_api_client/models/ring_configuration.py +++ b/oura_api_client/models/ring_configuration.py @@ -1,6 +1,8 @@ from pydantic import BaseModel, Field -from typing import List, Optional, Literal +from typing import List, Optional from datetime import datetime +# Enum-like fields will be handled with Literal +from typing import Literal class RingConfigurationModel(BaseModel): @@ -21,17 +23,33 @@ class RingConfigurationModel(BaseModel): "gucci", # New "heritage", "horizon" - ]] = Field(None, alias="design") + ]] = Field( + + None, alias="design" + + ) firmware_version: Optional[str] = Field(None, alias="firmware_version") hardware_type: Optional[Literal[ "gen1", "gen2", "gen2m", "gen3" - ]] = Field(None, alias="hardware_type") + ]] = Field( + + None, alias="hardware_type" + + ) # 'id' is already included - set_up_at: Optional[datetime] = Field(None, alias="set_up_at") # Changed from setup_at for Pythonic convention - size: Optional[int] = Field(None, alias="size") + set_up_at: Optional[datetime] = Field( + + None, alias="set_up_at" + + ) # Changed from setup_at for Pythonic convention + size: Optional[int] = Field( + + None, alias="size" + + ) # RingColor, RingDesign, RingHardwareType are effectively defined by Literals above # No separate models needed for them if they are just choices for a field. diff --git a/oura_api_client/models/session.py b/oura_api_client/models/session.py index b31257b..e7a7fd0 100644 --- a/oura_api_client/models/session.py +++ b/oura_api_client/models/session.py @@ -1,6 +1,6 @@ from pydantic import BaseModel, Field from typing import List, Optional -from datetime import datetime, date +from datetime import datetime, date # Added date for day field # MomentType and MomentMood are enums, but Pydantic uses Literal for this from typing import Literal @@ -9,7 +9,9 @@ class SessionModel(BaseModel): id: str day: date # Added day based on common patterns in other models - start_datetime: datetime = Field(alias="start_datetime") + start_datetime: datetime = Field( + alias="start_datetime" + ) end_datetime: datetime = Field(alias="end_datetime") type: Literal[ "breathing_exercise", @@ -37,16 +39,30 @@ class SessionModel(BaseModel): "tired", "undefined" ]] = None - heart_rate: Optional[str] = Field(None, alias="heart_rate") # Assuming string, adjust if complex - heart_rate_variability: Optional[str] = Field(None, alias="heart_rate_variability") # Assuming string - motion_count: Optional[int] = Field(None, alias="motion_count") + heart_rate: Optional[str] = Field( + None, alias="heart_rate" + ) # Assuming string, adjust if complex + heart_rate_variability: Optional[str] = Field( + + None, alias="heart_rate_variability" + + ) # Assuming string + motion_count: Optional[int] = Field( + + None, alias="motion_count" + + ) # New fields from OpenAPI spec for Session breathing_rate: Optional[float] = Field(None, alias="breathing_rate") duration: Optional[int] = Field(None, alias="duration") energy: Optional[float] = Field(None, alias="energy") hrv_data: Optional[str] = Field(None, alias="hrv_data") # Assuming string label: Optional[str] = Field(None, alias="label") - readiness_score_delta: Optional[int] = Field(None, alias="readiness_score_delta") + readiness_score_delta: Optional[int] = Field( + + None, alias="readiness_score_delta" + + ) skin_temperature: Optional[float] = Field(None, alias="skin_temperature") sleep_score_delta: Optional[int] = Field(None, alias="sleep_score_delta") stress: Optional[float] = Field(None, alias="stress") diff --git a/oura_api_client/models/sleep.py b/oura_api_client/models/sleep.py index 381a325..f31ce8e 100644 --- a/oura_api_client/models/sleep.py +++ b/oura_api_client/models/sleep.py @@ -7,38 +7,116 @@ class SleepModel(BaseModel): id: str - average_breath: Optional[float] = Field(None, alias="average_breath") # New based on common sleep metrics - average_heart_rate: Optional[float] = Field(None, alias="average_heart_rate") - average_hrv: Optional[int] = Field(None, alias="average_hrv") # Changed type to int based on typical HRV units + average_breath: Optional[float] = Field( + None, alias="average_breath" + ) # New based on common sleep metrics + average_heart_rate: Optional[float] = Field( + + None, alias="average_heart_rate" + + ) + average_hrv: Optional[int] = Field( + + None, alias="average_hrv" + + ) # Changed type to int based on typical HRV units awake_time: Optional[int] = Field(None, alias="awake_time") bedtime_end: Optional[datetime] = Field(None, alias="bedtime_end") bedtime_start: Optional[datetime] = Field(None, alias="bedtime_start") day: date # Added day - deep_sleep_duration: Optional[int] = Field(None, alias="deep_sleep_duration") + deep_sleep_duration: Optional[int] = Field( + + None, alias="deep_sleep_duration" + + ) efficiency: Optional[int] = Field(None, alias="efficiency") - heart_rate: Optional[str] = Field(None, alias="heart_rate") # Assuming string for heart_rate, adjust if it's a more complex type - hrv: Optional[str] = Field(None, alias="hrv") # Assuming string for hrv, adjust if it's a more complex type + heart_rate: Optional[str] = Field( + + None, alias="heart_rate" + + ) # Assuming string for heart_rate, adjust if it's a more complex type + hrv: Optional[str] = Field( + + None, alias="hrv" + + ) # Assuming string for hrv, adjust if it's a more complex type latency: Optional[int] = Field(None, alias="latency") - light_sleep_duration: Optional[int] = Field(None, alias="light_sleep_duration") + light_sleep_duration: Optional[int] = Field( + + None, alias="light_sleep_duration" + + ) low_battery_alert: Optional[bool] = Field(None, alias="low_battery_alert") - lowest_heart_rate: Optional[int] = Field(None, alias="lowest_heart_rate") # Changed type to int + lowest_heart_rate: Optional[int] = Field( + + None, alias="lowest_heart_rate" + + ) # Changed type to int movement_30_sec: Optional[str] = Field(None, alias="movement_30_sec") period: Optional[int] = Field(None, alias="period") - readiness: Optional[ReadinessContributors] = Field(None, alias="readiness") # Reused ReadinessContributors - readiness_score_delta: Optional[int] = Field(None, alias="readiness_score_delta") - rem_sleep_duration: Optional[int] = Field(None, alias="rem_sleep_duration") - restless_periods: Optional[int] = Field(None, alias="restless_periods") # Added from daily_sleep + readiness: Optional[ReadinessContributors] = Field( + + None, alias="readiness" + + ) # Reused ReadinessContributors + readiness_score_delta: Optional[int] = Field( + + None, alias="readiness_score_delta" + + ) + rem_sleep_duration: Optional[int] = Field( + + None, alias="rem_sleep_duration" + + ) + restless_periods: Optional[int] = Field( + + None, alias="restless_periods" + + ) # Added from daily_sleep # score is usually part of daily summaries, but can be part of a detailed sleep document - score: Optional[int] = Field(None, alias="score") + score: Optional[int] = Field( + + None, alias="score" + + ) sleep_phase_5_min: Optional[str] = Field(None, alias="sleep_phase_5_min") - sleep_score_delta: Optional[int] = Field(None, alias="sleep_score_delta") # New, similar to readiness_score_delta - sleep_algorithm_version: Optional[str] = Field(None, alias="sleep_algorithm_version") # New - temperature_delta: Optional[float] = Field(None, alias="temperature_delta") - temperature_deviation: Optional[float] = Field(None, alias="temperature_deviation") # Deprecated in daily_readiness - temperature_trend_deviation: Optional[float] = Field(None, alias="temperature_trend_deviation") # From daily_readiness + sleep_score_delta: Optional[int] = Field( + + None, alias="sleep_score_delta" + + ) # New, similar to readiness_score_delta + sleep_algorithm_version: Optional[str] = Field( + + None, alias="sleep_algorithm_version" + + ) # New + temperature_delta: Optional[float] = Field( + + None, alias="temperature_delta" + + ) + temperature_deviation: Optional[float] = Field( + + None, alias="temperature_deviation" + + ) # Deprecated in daily_readiness + temperature_trend_deviation: Optional[float] = Field( + + None, alias="temperature_trend_deviation" + + ) # From daily_readiness time_in_bed: Optional[int] = Field(None, alias="time_in_bed") - total_sleep_duration: Optional[int] = Field(None, alias="total_sleep_duration") - type: Optional[str] = Field(None, alias="type") # From daily_sleep (e.g. "main_sleep", "nap") + total_sleep_duration: Optional[int] = Field( + + None, alias="total_sleep_duration" + + ) + type: Optional[str] = Field( + + None, alias="type" + + ) # From daily_sleep (e.g. "main_sleep", "nap") # contributors from daily_sleep.py, as requested by the task contributors: SleepContributors diff --git a/oura_api_client/models/sleep_time.py b/oura_api_client/models/sleep_time.py index 2474e01..8e2a28c 100644 --- a/oura_api_client/models/sleep_time.py +++ b/oura_api_client/models/sleep_time.py @@ -1,15 +1,25 @@ from pydantic import BaseModel, Field from typing import List, Optional -from datetime import date, datetime +from datetime import date, datetime # Added datetime # Enum-like fields will be handled with Literal as per previous patterns if needed, # but based on the provided snippet, direct enum models are not explicitly requested here. class SleepTimeWindow(BaseModel): - day_light_saving_time: Optional[int] = Field(None, alias="day_light_saving_time") # New - end_offset: Optional[int] = Field(None, alias="end_offset") # Offset from midnight in seconds - start_offset: Optional[int] = Field(None, alias="start_offset") # Offset from midnight in seconds + day_light_saving_time: Optional[int] = Field( + None, alias="day_light_saving_time" + ) # New + end_offset: Optional[int] = Field( + + None, alias="end_offset" + + ) # Offset from midnight in seconds + start_offset: Optional[int] = Field( + + None, alias="start_offset" + + ) # Offset from midnight in seconds class SleepTimeRecommendation(BaseModel): @@ -31,11 +41,25 @@ class SleepTimeStatus(BaseModel): class SleepTimeModel(BaseModel): id: str # Though API doc says no ID, a unique identifier per record is standard day: date - optimal_bedtime: Optional[SleepTimeWindow] = Field(None, alias="optimal_bedtime") - recommendation: Optional[SleepTimeRecommendation] = Field(None, alias="recommendation") # Using the new model - status: Optional[SleepTimeStatus] = Field(None, alias="status") # Using the new model + optimal_bedtime: Optional[SleepTimeWindow] = Field( + None, alias="optimal_bedtime" + ) + recommendation: Optional[SleepTimeRecommendation] = Field( + + None, alias="recommendation" + + ) # Using the new model + status: Optional[SleepTimeStatus] = Field( + + None, alias="status" + + ) # Using the new model # Assuming timestamp might be relevant for when the record was created or last updated - timestamp: Optional[datetime] = Field(None, alias="timestamp") # Added timestamp + timestamp: Optional[datetime] = Field( + + None, alias="timestamp" + + ) # Added timestamp class SleepTimeResponse(BaseModel): diff --git a/oura_api_client/models/tag.py b/oura_api_client/models/tag.py index 8a080f7..983b89a 100644 --- a/oura_api_client/models/tag.py +++ b/oura_api_client/models/tag.py @@ -1,6 +1,6 @@ from pydantic import BaseModel from typing import List, Optional -from datetime import date, datetime +from datetime import date, datetime # Added datetime class TagModel(BaseModel): @@ -9,10 +9,12 @@ class TagModel(BaseModel): text: Optional[str] = None # Optional based on OpenAPI spec timestamp: datetime # Changed from Optional[datetime] to datetime as it's usually present # New fields from OpenAPI spec for Tag - # Assuming 'tag_type_code' and 'start_time', 'end_time' might be part of a more detailed spec, + # Assuming 'tag_type_code' and 'start_time', 'end_time' might be part + # of a more detailed spec, # but the provided snippet for Tag is simple. # For now, sticking to id, day, text, timestamp as core. - # If more fields like 'tag_type_code', 'start_time', 'end_time' are confirmed, they can be added. + # If more fields like 'tag_type_code', 'start_time', 'end_time' are + # confirmed, they can be added. # Example of what they might look like if added: # tag_type_code: Optional[str] = Field(None, alias="tag_type_code") # start_time: Optional[datetime] = Field(None, alias="start_time") diff --git a/oura_api_client/models/vo2_max.py b/oura_api_client/models/vo2_max.py index 258cc4a..875484e 100644 --- a/oura_api_client/models/vo2_max.py +++ b/oura_api_client/models/vo2_max.py @@ -6,10 +6,16 @@ class Vo2MaxModel(BaseModel): id: str day: date - vo2_max: Optional[float] = Field(None) # User's estimated VO2 max in mL/kg/min + vo2_max: Optional[float] = Field( + None, alias="vo2_max" + ) # User's estimated VO2 max in mL/kg/min # Based on typical VO2 max reports, additional context might be provided: - # fitness_level: Optional[str] = Field(None) # e.g., "excellent", "good", "fair" - # source: Optional[str] = Field(None) # e.g., "estimated_from_workout", "manual_entry" + # fitness_level: Optional[str] = Field( + # None, alias="fitness_level" + # ) # e.g., "excellent", "good", "fair" + # source: Optional[str] = Field( + # None, alias="source" + # ) # e.g., "estimated_from_workout", "manual_entry" timestamp: datetime # Timestamp of the summary diff --git a/oura_api_client/models/webhook.py b/oura_api_client/models/webhook.py index 4165fc9..ba7c4c9 100644 --- a/oura_api_client/models/webhook.py +++ b/oura_api_client/models/webhook.py @@ -1,32 +1,9 @@ from pydantic import BaseModel, Field -from typing import List, Optional +from typing import Optional, List 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 WebhookSubscriptionModel(BaseModel): - id: str # Webhook subscription ID - created_at: datetime = Field(alias="created_at") - updated_at: datetime = Field(alias="updated_at") # Optional, as it might not be updated - verification_token: Optional[str] = Field(alias="verification_token") # Only present on creation/update - callback_url: str = Field(alias="callback_url") - subscribed_events: Optional[List[WebhookEventModel]] = Field(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(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(alias="event_types") - - class WebhookOperation(str, Enum): CREATE = "create" UPDATE = "update" @@ -56,29 +33,63 @@ class ExtApiV2DataType(str, Enum): class WebhookSubscriptionModel(BaseModel): - id: str = Field(..., description="Webhook subscription ID") + 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") + 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") + 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: str = Field(..., alias="verification_token") # Made required as per spec +class WebhookSubscriptionCreateRequest(BaseModel): # For POST request body + 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 +class WebhookSubscriptionUpdateRequest(BaseModel): # For PUT request body + verification_token: str = Field( + + + + ..., alias="verification_token" + + + +) # Required callback_url: Optional[str] = Field(None, alias="callback_url") event_type: Optional[WebhookOperation] = Field(None, alias="event_type") - data_type: Optional[ExtApiV2DataType] = Field(None, alias="data_type") + data_type: Optional[ExtApiV2DataType] = Field( + + None, alias="data_type" + + ) # No longer using WebhookListResponse as the API returns a direct list. # class WebhookListResponse(BaseModel): diff --git a/oura_api_client/models/workout.py b/oura_api_client/models/workout.py index bbf1f6a..37b8fb7 100644 --- a/oura_api_client/models/workout.py +++ b/oura_api_client/models/workout.py @@ -1,6 +1,6 @@ from pydantic import BaseModel, Field from typing import List, Optional -from datetime import date, datetime +from datetime import date, datetime # Added date # WorkoutIntensity and WorkoutSource are enums, but Pydantic uses Literal for this from typing import Literal @@ -29,11 +29,15 @@ class WorkoutModel(BaseModel): "strava", # New based on common integrations "oura_app" # New for workouts logged directly in Oura ] - start_datetime: datetime = Field(alias="start_datetime") + start_datetime: datetime = Field( + alias="start_datetime" + ) # New fields from OpenAPI spec for Workout, if any, would be added here. # For now, using a common set of fields for workout tracking. # Example: - # route_coordinates: Optional[str] = Field(None, alias="route_coordinates") # If GPS data was available + # route_coordinates: Optional[str] = Field( + # None, alias="route_coordinates" + # ) # If GPS data was available class WorkoutResponse(BaseModel): diff --git a/tests/test_client.py b/tests/test_client.py index 56b268b..f9460fb 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -6,25 +6,46 @@ from oura_api_client.api.client import OuraClient from oura_api_client.models.heartrate import HeartRateResponse -from oura_api_client.models.daily_activity import DailyActivityResponse, DailyActivityModel, ActivityContributors -from oura_api_client.models.daily_sleep import DailySleepResponse, DailySleepModel, SleepContributors -from oura_api_client.models.daily_readiness import DailyReadinessResponse, DailyReadinessModel, ReadinessContributors +from oura_api_client.models.daily_activity import ( + DailyActivityResponse, DailyActivityModel, ActivityContributors +) +from oura_api_client.models.daily_sleep import ( + DailySleepResponse, DailySleepModel, SleepContributors +) +from oura_api_client.models.daily_readiness import ( + DailyReadinessResponse, DailyReadinessModel, ReadinessContributors +) from oura_api_client.models.sleep import SleepResponse, SleepModel from oura_api_client.models.session import SessionResponse, SessionModel from oura_api_client.models.tag import TagResponse, TagModel from oura_api_client.models.workout import WorkoutResponse, WorkoutModel -from oura_api_client.models.enhanced_tag import EnhancedTagResponse, EnhancedTagModel -from oura_api_client.models.daily_spo2 import DailySpO2Response, DailySpO2Model, DailySpO2AggregatedValuesModel -from oura_api_client.models.sleep_time import SleepTimeResponse, SleepTimeModel, SleepTimeWindow, SleepTimeRecommendation, SleepTimeStatus -from oura_api_client.models.rest_mode_period import RestModePeriodResponse, RestModePeriodModel # Added RestModePeriod models -from oura_api_client.models.daily_stress import DailyStressResponse, DailyStressModel, DailyStressSummary # Added DailyStress models -from oura_api_client.models.daily_resilience import DailyResilienceResponse, DailyResilienceModel, ResilienceContributors, LongTermResilienceLevel # Added DailyResilience models -from oura_api_client.models.daily_cardiovascular_age import DailyCardiovascularAgeResponse, DailyCardiovascularAgeModel # Added DailyCardiovascularAge models -from oura_api_client.models.vo2_max import Vo2MaxResponse, Vo2MaxModel # Added Vo2Max models +from oura_api_client.models.enhanced_tag import ( + EnhancedTagResponse, EnhancedTagModel +) +from oura_api_client.models.daily_spo2 import ( + DailySpO2Response, DailySpO2Model, DailySpO2AggregatedValuesModel +) +from oura_api_client.models.sleep_time import ( + SleepTimeResponse, SleepTimeModel, SleepTimeWindow, + SleepTimeRecommendation, SleepTimeStatus +) +from oura_api_client.models.rest_mode_period import ( + RestModePeriodResponse, RestModePeriodModel +) # Added RestModePeriod models +from oura_api_client.models.daily_stress import ( + DailyStressResponse, DailyStressModel +) # Added DailyStress models +from oura_api_client.models.daily_resilience import ( + DailyResilienceResponse, DailyResilienceModel +) # Added DailyResilience models +from oura_api_client.models.daily_cardiovascular_age import ( + DailyCardiovascularAgeResponse, DailyCardiovascularAgeModel +) # Added DailyCardiovascularAge models +from oura_api_client.models.vo2_max import ( + Vo2MaxResponse, Vo2MaxModel +) # Added Vo2Max models import requests from requests.exceptions import RequestException -import httpretty # Import httpretty for more robust mocking if needed, or stick to unittest.mock - class TestOuraClient(unittest.TestCase): """Test the OuraClient class.""" @@ -36,7 +57,9 @@ def setUp(self): def test_initialization(self): """Test that the client initializes correctly.""" self.assertEqual(self.client.access_token, "test_token") - self.assertEqual(self.client.headers["Authorization"], "Bearer test_token") + self.assertEqual( + self.client.headers["Authorization"], "Bearer test_token" + ) self.assertIsNotNone(self.client.heartrate) self.assertIsNotNone(self.client.personal) self.assertIsNotNone(self.client.daily_activity) @@ -49,11 +72,16 @@ def test_initialization(self): self.assertIsNotNone(self.client.enhanced_tag) self.assertIsNotNone(self.client.daily_spo2) self.assertIsNotNone(self.client.sleep_time) - self.assertIsNotNone(self.client.rest_mode_period) # Added rest_mode_period - self.assertIsNotNone(self.client.daily_stress) # Added daily_stress - self.assertIsNotNone(self.client.daily_resilience) # Added daily_resilience - self.assertIsNotNone(self.client.daily_cardiovascular_age) # Added daily_cardiovascular_age - self.assertIsNotNone(self.client.vo2_max) # Added vo2_max + # Added rest_mode_period + self.assertIsNotNone(self.client.rest_mode_period) + # Added daily_stress + self.assertIsNotNone(self.client.daily_stress) + # Added daily_resilience + self.assertIsNotNone(self.client.daily_resilience) + # Added daily_cardiovascular_age + self.assertIsNotNone(self.client.daily_cardiovascular_age) + # Added vo2_max + self.assertIsNotNone(self.client.vo2_max) @patch("requests.get") def test_get_heart_rate(self, mock_get): @@ -63,7 +91,11 @@ def test_get_heart_rate(self, mock_get): mock_response.raise_for_status.return_value = None mock_response.json.return_value = { "data": [ - {"timestamp": "2024-03-01T12:00:00+00:00", "bpm": 75, "source": "test"} + { + "timestamp": "2024-03-01T12:00:00+00:00", + "bpm": 75, + "source": "test" + } ], "next_token": None, } @@ -90,13 +122,12 @@ def test_get_heart_rate(self, mock_get): params={"start_date": "2024-03-01", "end_date": "2024-03-15"}, ) - if __name__ == "__main__": unittest.main() - class TestDailyActivity(unittest.TestCase): def setUp(self): + self.client = OuraClient(access_token="test_token") @patch("requests.get") @@ -113,11 +144,16 @@ def test_get_daily_activity_documents(self, mock_get): "timestamp": "2024-03-11T00:00:00+00:00", }, ] - mock_response_json = {"data": mock_data, "next_token": "test_next_token"} + mock_response_json = { + "data": mock_data, + "next_token": "test_next_token" + } # Configure the mock_get object to simulate a successful response mock_response = MagicMock() - mock_response.raise_for_status.return_value = None # Simulate no HTTP error - mock_response.json.return_value = mock_response_json # Set the JSON response + # Simulate no HTTP error + mock_response.raise_for_status.return_value = None + # Set the JSON response + mock_response.json.return_value = mock_response_json mock_get.return_value = mock_response start_date_str = "2024-03-10" @@ -125,17 +161,27 @@ def test_get_daily_activity_documents(self, mock_get): start_date = date.fromisoformat(start_date_str) end_date = date.fromisoformat(end_date_str) - daily_activity_response = self.client.daily_activity.get_daily_activity_documents( - start_date=start_date, end_date=end_date, next_token="test_token" + daily_activity_response = ( + self.client.daily_activity.get_daily_activity_documents( + start_date=start_date, + end_date=end_date, + next_token="test_token" + ) ) self.assertIsInstance(daily_activity_response, DailyActivityResponse) self.assertEqual(len(daily_activity_response.data), 2) - self.assertIsInstance(daily_activity_response.data[0], DailyActivityModel) - self.assertEqual(daily_activity_response.next_token, "test_next_token") - # Use self.client.client._make_request for assertion if client.get is not available + self.assertIsInstance( + daily_activity_response.data[0], DailyActivityModel + ) + self.assertEqual( + daily_activity_response.next_token, "test_next_token" + ) + # Use self.client.client._make_request for assertion + # if client.get is not available actual_call_url = mock_get.call_args[0][0] - expected_url = f"{self.client.BASE_URL}/v2/usercollection/daily_activity" + base_url = self.client.BASE_URL + expected_url = f"{base_url}/v2/usercollection/daily_activity" self.assertTrue(actual_call_url.endswith(expected_url)) called_params = mock_get.call_args[1]['params'] @@ -168,7 +214,8 @@ def test_get_daily_activity_documents_with_string_dates(self, mock_get): start_date=start_date_str, end_date=end_date_str ) actual_call_url = mock_get.call_args[0][0] - expected_url = f"{self.client.BASE_URL}/v2/usercollection/daily_activity" + base_url = self.client.BASE_URL + expected_url = f"{base_url}/v2/usercollection/daily_activity" self.assertTrue(actual_call_url.endswith(expected_url)) called_params = mock_get.call_args[1]['params'] @@ -235,7 +282,8 @@ def test_get_daily_activity_document(self, mock_get): self.assertIsInstance(daily_activity_document.contributors, ActivityContributors) actual_call_url = mock_get.call_args[0][0] - expected_url = f"{self.client.BASE_URL}/v2/usercollection/daily_activity/{document_id}" + base_url = self.client.BASE_URL + expected_url = f"{base_url}/v2/usercollection/daily_activity/{document_id}" self.assertTrue(actual_call_url.endswith(expected_url)) called_params = mock_get.call_args[1]['params'] @@ -248,9 +296,9 @@ def test_get_daily_activity_document_error(self, mock_get): with self.assertRaises(RequestException): self.client.daily_activity.get_daily_activity_document(document_id=document_id) - class TestDailySleep(unittest.TestCase): def setUp(self): + self.client = OuraClient(access_token="test_token") @patch("requests.get") @@ -296,14 +344,21 @@ def test_get_daily_sleep_documents(self, mock_get): start_date = date.fromisoformat(start_date_str) end_date = date.fromisoformat(end_date_str) - daily_sleep_response = self.client.daily_sleep.get_daily_sleep_documents( - start_date=start_date, end_date=end_date, next_token="test_sleep_token" + daily_sleep_response = ( + self.client.daily_sleep.get_daily_sleep_documents( + start_date=start_date, + end_date=end_date, + next_token="test_sleep_token" + ) ) self.assertIsInstance(daily_sleep_response, DailySleepResponse) self.assertEqual(len(daily_sleep_response.data), 2) self.assertIsInstance(daily_sleep_response.data[0], DailySleepModel) - self.assertIsInstance(daily_sleep_response.data[0].contributors, SleepContributors) + self.assertIsInstance( + daily_sleep_response.data[0].contributors, + SleepContributors + ) self.assertEqual(daily_sleep_response.next_token, "next_sleep_token") mock_get.assert_called_once_with( @@ -314,6 +369,7 @@ def test_get_daily_sleep_documents(self, mock_get): "end_date": end_date_str, "next_token": "test_sleep_token", }, + ) @patch("requests.get") @@ -343,6 +399,7 @@ def test_get_daily_sleep_documents_with_string_dates(self, mock_get): f"{self.client.BASE_URL}/v2/usercollection/daily_sleep", headers=self.client.headers, params={"start_date": start_date_str, "end_date": end_date_str}, + ) @patch("requests.get") @@ -385,15 +442,24 @@ def test_get_daily_sleep_document(self, mock_get): self.assertIsInstance(daily_sleep_document, DailySleepModel) self.assertEqual(daily_sleep_document.id, document_id) - self.assertIsInstance(daily_sleep_document.contributors, SleepContributors) + self.assertIsInstance( + daily_sleep_document.contributors, SleepContributors + ) self.assertEqual(daily_sleep_document.score, 85) - self.assertEqual(daily_sleep_document.bedtime_end, datetime.fromisoformat("2024-03-11T07:00:00+00:00")) - self.assertEqual(daily_sleep_document.bedtime_start, datetime.fromisoformat("2024-03-10T22:00:00+00:00")) + self.assertEqual( + daily_sleep_document.bedtime_end, + datetime.fromisoformat("2024-03-11T07:00:00+00:00") + ) + self.assertEqual( + daily_sleep_document.bedtime_start, + datetime.fromisoformat("2024-03-10T22:00:00+00:00") + ) mock_get.assert_called_once_with( f"{self.client.BASE_URL}/v2/usercollection/daily_sleep/{document_id}", headers=self.client.headers, params=None, + ) @patch("requests.get") @@ -403,9 +469,9 @@ def test_get_daily_sleep_document_error(self, mock_get): with self.assertRaises(RequestException): self.client.daily_sleep.get_daily_sleep_document(document_id=document_id) - class TestDailyReadiness(unittest.TestCase): def setUp(self): + self.client = OuraClient(access_token="test_token") @patch("requests.get") @@ -437,15 +503,26 @@ def test_get_daily_readiness_documents(self, mock_get): start_date = date.fromisoformat(start_date_str) end_date = date.fromisoformat(end_date_str) - daily_readiness_response = self.client.daily_readiness.get_daily_readiness_documents( - start_date=start_date, end_date=end_date, next_token="test_readiness_token" + daily_readiness_response = ( + self.client.daily_readiness.get_daily_readiness_documents( + start_date=start_date, + end_date=end_date, + next_token="test_readiness_token" + ) ) self.assertIsInstance(daily_readiness_response, DailyReadinessResponse) self.assertEqual(len(daily_readiness_response.data), 2) - self.assertIsInstance(daily_readiness_response.data[0], DailyReadinessModel) - self.assertIsInstance(daily_readiness_response.data[0].contributors, ReadinessContributors) - self.assertEqual(daily_readiness_response.next_token, "next_readiness_token") + self.assertIsInstance( + daily_readiness_response.data[0], DailyReadinessModel + ) + self.assertIsInstance( + daily_readiness_response.data[0].contributors, + ReadinessContributors + ) + self.assertEqual( + daily_readiness_response.next_token, "next_readiness_token" + ) mock_get.assert_called_once_with( f"{self.client.BASE_URL}/v2/usercollection/daily_readiness", @@ -455,6 +532,7 @@ def test_get_daily_readiness_documents(self, mock_get): "end_date": end_date_str, "next_token": "test_readiness_token", }, + ) @patch("requests.get") @@ -485,6 +563,7 @@ def test_get_daily_readiness_documents_with_string_dates(self, mock_get): f"{self.client.BASE_URL}/v2/usercollection/daily_readiness", headers=self.client.headers, params={"start_date": start_date_str, "end_date": end_date_str}, + ) @patch("requests.get") @@ -523,15 +602,23 @@ def test_get_daily_readiness_document(self, mock_get): mock_get.return_value = mock_response document_id = "test_readiness_document_id" - daily_readiness_document = self.client.daily_readiness.get_daily_readiness_document( - document_id=document_id + daily_readiness_document = ( + self.client.daily_readiness.get_daily_readiness_document( + document_id=document_id + ) ) self.assertIsInstance(daily_readiness_document, DailyReadinessModel) self.assertEqual(daily_readiness_document.id, document_id) - self.assertIsInstance(daily_readiness_document.contributors, ReadinessContributors) + self.assertIsInstance( + daily_readiness_document.contributors, + ReadinessContributors + ) self.assertEqual(daily_readiness_document.score, 78) - self.assertEqual(daily_readiness_document.activity_class_5_min, "some_activity_class") + self.assertEqual( + daily_readiness_document.activity_class_5_min, + "some_activity_class" + ) self.assertEqual(daily_readiness_document.hrv_balance_data, "some_hrv_data") self.assertEqual(daily_readiness_document.spo2_percentage, 98.5) @@ -539,6 +626,7 @@ def test_get_daily_readiness_document(self, mock_get): f"{self.client.BASE_URL}/v2/usercollection/daily_readiness/{document_id}", headers=self.client.headers, params=None, + ) @patch("requests.get") @@ -546,23 +634,29 @@ def test_get_daily_readiness_document_error(self, mock_get): mock_get.side_effect = RequestException("API error") document_id = "test_readiness_document_id" with self.assertRaises(RequestException): - self.client.daily_readiness.get_daily_readiness_document(document_id=document_id) - + self.client.daily_readiness.get_daily_readiness_document( + document_id=document_id + ) class TestSleep(unittest.TestCase): def setUp(self): + self.client = OuraClient(access_token="test_token") @patch("requests.get") def test_get_sleep_documents(self, mock_get): # Reused from DailySleep for consistency mock_contributors_data = { - "deep_sleep": 70, "efficiency": 80, "latency": 90, "rem_sleep": 60, + "deep_sleep": 70, "efficiency": 80, + "latency": 90, "rem_sleep": 60, "restfulness": 75, "timing": 85, "total_sleep": 95, } - mock_readiness_contributors_data = { # Reused from DailyReadiness - "activity_balance": 60, "body_temperature": 70, "hrv_balance": 80, - "previous_day_activity": 90, "previous_night": 50, "recovery_index": 65, + # Reused from DailyReadiness + mock_readiness_contributors_data = { + "activity_balance": 60, "body_temperature": 70, + "hrv_balance": 80, + "previous_day_activity": 90, "previous_night": 50, + "recovery_index": 65, "resting_heart_rate": 75, "sleep_balance": 85, } mock_data = [ @@ -588,7 +682,10 @@ def test_get_sleep_documents(self, mock_get): "timestamp": "2024-03-10T22:00:00+00:00", # Added timestamp for SleepModel }, ] - mock_response_json = {"data": mock_data, "next_token": "next_sleep_doc_token"} + mock_response_json = { + "data": mock_data, + "next_token": "next_sleep_doc_token" + } mock_response = MagicMock() mock_response.raise_for_status.return_value = None mock_response.json.return_value = mock_response_json @@ -600,14 +697,23 @@ def test_get_sleep_documents(self, mock_get): end_date = date.fromisoformat(end_date_str) sleep_response = self.client.sleep.get_sleep_documents( - start_date=start_date, end_date=end_date, next_token="test_sleep_doc_token" + start_date=start_date, + end_date=end_date, + next_token="test_sleep_doc_token" ) self.assertIsInstance(sleep_response, SleepResponse) self.assertEqual(len(sleep_response.data), 1) self.assertIsInstance(sleep_response.data[0], SleepModel) - self.assertIsInstance(sleep_response.data[0].contributors, SleepContributors) - self.assertIsInstance(sleep_response.data[0].readiness, ReadinessContributors) + self.assertIsInstance( + + sleep_response.data[0].contributors, + SleepContributors + + ) + self.assertIsInstance( + sleep_response.data[0].readiness, ReadinessContributors + ) self.assertEqual(sleep_response.next_token, "next_sleep_doc_token") mock_get.assert_called_once_with( @@ -618,12 +724,18 @@ def test_get_sleep_documents(self, mock_get): "end_date": end_date_str, "next_token": "test_sleep_doc_token", }, + ) @patch("requests.get") def test_get_sleep_documents_with_string_dates(self, mock_get): # Simplified mock data for this test - mock_data = [{"id": "sleep_doc_str_date", "day": "2024-03-10", "contributors": {"deep_sleep": 1}, "timestamp": "2024-03-10T22:00:00+00:00"}] + mock_data = [{ + "id": "sleep_doc_str_date", + "day": "2024-03-10", + "contributors": {"deep_sleep": 1}, + "timestamp": "2024-03-10T22:00:00+00:00" + }] mock_response_json = {"data": mock_data, "next_token": None} mock_response = MagicMock() mock_response.raise_for_status.return_value = None @@ -641,6 +753,7 @@ def test_get_sleep_documents_with_string_dates(self, mock_get): f"{self.client.BASE_URL}/v2/usercollection/sleep", headers=self.client.headers, params={"start_date": start_date_str, "end_date": end_date_str}, + ) @patch("requests.get") @@ -673,19 +786,25 @@ def test_get_sleep_document(self, mock_get): mock_get.return_value = mock_response document_id = "test_sleep_doc_single" - sleep_document = self.client.sleep.get_sleep_document(document_id=document_id) + sleep_document = self.client.sleep.get_sleep_document( + document_id=document_id + ) self.assertIsInstance(sleep_document, SleepModel) self.assertEqual(sleep_document.id, document_id) self.assertIsInstance(sleep_document.contributors, SleepContributors) self.assertIsInstance(sleep_document.readiness, ReadinessContributors) self.assertEqual(sleep_document.score, 88) - self.assertEqual(sleep_document.bedtime_end, datetime.fromisoformat("2024-03-11T07:30:00+00:00")) + self.assertEqual( + sleep_document.bedtime_end, + datetime.fromisoformat("2024-03-11T07:30:00+00:00") + ) mock_get.assert_called_once_with( f"{self.client.BASE_URL}/v2/usercollection/sleep/{document_id}", headers=self.client.headers, params=None, + ) @patch("requests.get") @@ -695,9 +814,9 @@ def test_get_sleep_document_error(self, mock_get): with self.assertRaises(RequestException): self.client.sleep.get_sleep_document(document_id=document_id) - class TestSession(unittest.TestCase): def setUp(self): + self.client = OuraClient(access_token="test_token") @patch("requests.get") @@ -733,7 +852,9 @@ def test_get_session_documents(self, mock_get): end_date = date.fromisoformat(end_date_str) session_response = self.client.session.get_session_documents( - start_date=start_date, end_date=end_date, next_token="test_session_token" + start_date=start_date, + end_date=end_date, + next_token="test_session_token" ) self.assertIsInstance(session_response, SessionResponse) @@ -749,6 +870,7 @@ def test_get_session_documents(self, mock_get): "end_date": end_date_str, "next_token": "test_session_token", }, + ) @patch("requests.get") @@ -779,6 +901,7 @@ def test_get_session_documents_with_string_dates(self, mock_get): f"{self.client.BASE_URL}/v2/usercollection/session", headers=self.client.headers, params={"start_date": start_date_str, "end_date": end_date_str}, + ) @patch("requests.get") @@ -808,19 +931,25 @@ def test_get_session_document(self, mock_get): mock_get.return_value = mock_response document_id = "test_session_single" - session_document = self.client.session.get_session_document(document_id=document_id) + session_document = self.client.session.get_session_document( + document_id=document_id + ) self.assertIsInstance(session_document, SessionModel) self.assertEqual(session_document.id, document_id) self.assertEqual(session_document.type, "workout") self.assertEqual(session_document.mood, "great") self.assertEqual(session_document.duration, 2700) - self.assertEqual(session_document.start_datetime, datetime.fromisoformat("2024-03-10T15:00:00+00:00")) + self.assertEqual( + session_document.start_datetime, + datetime.fromisoformat("2024-03-10T15:00:00+00:00") + ) mock_get.assert_called_once_with( f"{self.client.BASE_URL}/v2/usercollection/session/{document_id}", headers=self.client.headers, params=None, + ) @patch("requests.get") @@ -830,9 +959,9 @@ def test_get_session_document_error(self, mock_get): with self.assertRaises(RequestException): self.client.session.get_session_document(document_id=document_id) - class TestTag(unittest.TestCase): def setUp(self): + self.client = OuraClient(access_token="test_token") @patch("requests.get") @@ -863,7 +992,9 @@ def test_get_tag_documents(self, mock_get): end_date = date.fromisoformat(end_date_str) tag_response = self.client.tag.get_tag_documents( - start_date=start_date, end_date=end_date, next_token="test_tag_token" + start_date=start_date, + end_date=end_date, + next_token="test_tag_token" ) self.assertIsInstance(tag_response, TagResponse) @@ -879,6 +1010,7 @@ def test_get_tag_documents(self, mock_get): "end_date": end_date_str, "next_token": "test_tag_token", }, + ) @patch("requests.get") @@ -908,6 +1040,7 @@ def test_get_tag_documents_with_string_dates(self, mock_get): f"{self.client.BASE_URL}/v2/usercollection/tag", headers=self.client.headers, params={"start_date": start_date_str, "end_date": end_date_str}, + ) @patch("requests.get") @@ -932,17 +1065,23 @@ def test_get_tag_document(self, mock_get): mock_get.return_value = mock_response document_id = "test_tag_single" - tag_document = self.client.tag.get_tag_document(document_id=document_id) + tag_document = self.client.tag.get_tag_document( + document_id=document_id + ) self.assertIsInstance(tag_document, TagModel) self.assertEqual(tag_document.id, document_id) self.assertEqual(tag_document.text, "Single tag test") - self.assertEqual(tag_document.timestamp, datetime.fromisoformat("2024-03-10T11:00:00+00:00")) + self.assertEqual( + tag_document.timestamp, + datetime.fromisoformat("2024-03-10T11:00:00+00:00") + ) mock_get.assert_called_once_with( f"{self.client.BASE_URL}/v2/usercollection/tag/{document_id}", headers=self.client.headers, params=None, + ) @patch("requests.get") @@ -952,9 +1091,9 @@ def test_get_tag_document_error(self, mock_get): with self.assertRaises(RequestException): self.client.tag.get_tag_document(document_id=document_id) - class TestWorkout(unittest.TestCase): def setUp(self): + self.client = OuraClient(access_token="test_token") @patch("requests.get") @@ -993,7 +1132,9 @@ def test_get_workout_documents(self, mock_get): end_date = date.fromisoformat(end_date_str) workout_response = self.client.workout.get_workout_documents( - start_date=start_date, end_date=end_date, next_token="test_workout_token" + start_date=start_date, + end_date=end_date, + next_token="test_workout_token" ) self.assertIsInstance(workout_response, WorkoutResponse) @@ -1009,6 +1150,7 @@ def test_get_workout_documents(self, mock_get): "end_date": end_date_str, "next_token": "test_workout_token", }, + ) @patch("requests.get") @@ -1041,6 +1183,7 @@ def test_get_workout_documents_with_string_dates(self, mock_get): f"{self.client.BASE_URL}/v2/usercollection/workout", headers=self.client.headers, params={"start_date": start_date_str, "end_date": end_date_str}, + ) @patch("requests.get") @@ -1072,19 +1215,25 @@ def test_get_workout_document(self, mock_get): mock_get.return_value = mock_response document_id = "test_workout_single" - workout_document = self.client.workout.get_workout_document(document_id=document_id) + workout_document = self.client.workout.get_workout_document( + document_id=document_id + ) self.assertIsInstance(workout_document, WorkoutModel) self.assertEqual(workout_document.id, document_id) self.assertEqual(workout_document.activity, "swimming") self.assertEqual(workout_document.intensity, "moderate") self.assertEqual(workout_document.source, "apple_health") - self.assertEqual(workout_document.start_datetime, datetime.fromisoformat("2024-03-10T12:00:00+00:00")) + self.assertEqual( + workout_document.start_datetime, + datetime.fromisoformat("2024-03-10T12:00:00+00:00") + ) mock_get.assert_called_once_with( f"{self.client.BASE_URL}/v2/usercollection/workout/{document_id}", headers=self.client.headers, params=None, + ) @patch("requests.get") @@ -1094,9 +1243,9 @@ def test_get_workout_document_error(self, mock_get): with self.assertRaises(RequestException): self.client.workout.get_workout_document(document_id=document_id) - class TestEnhancedTag(unittest.TestCase): def setUp(self): + self.client = OuraClient(access_token="test_token") @patch("requests.get") @@ -1119,7 +1268,10 @@ def test_get_enhanced_tag_documents(self, mock_get): "comment": "Beach time!" }, ] - mock_response_json = {"data": mock_data, "next_token": "next_enhanced_tag_token"} + mock_response_json = { + "data": mock_data, + "next_token": "next_enhanced_tag_token" + } mock_response = MagicMock() mock_response.raise_for_status.return_value = None mock_response.json.return_value = mock_response_json @@ -1131,15 +1283,23 @@ def test_get_enhanced_tag_documents(self, mock_get): end_date = date.fromisoformat(end_date_str) enhanced_tag_response = self.client.enhanced_tag.get_enhanced_tag_documents( - start_date=start_date, end_date=end_date, next_token="test_enhanced_tag_token" + start_date=start_date, + end_date=end_date, + next_token="test_enhanced_tag_token" ) self.assertIsInstance(enhanced_tag_response, EnhancedTagResponse) self.assertEqual(len(enhanced_tag_response.data), 2) - self.assertIsInstance(enhanced_tag_response.data[0], EnhancedTagModel) - self.assertEqual(enhanced_tag_response.next_token, "next_enhanced_tag_token") + self.assertIsInstance( + enhanced_tag_response.data[0], EnhancedTagModel + ) + self.assertEqual( + enhanced_tag_response.next_token, "next_enhanced_tag_token" + ) self.assertEqual(enhanced_tag_response.data[0].tag_type_code, "common_cold") - self.assertEqual(enhanced_tag_response.data[1].start_day, date(2024, 3, 15)) + self.assertEqual( + enhanced_tag_response.data[1].start_day, date(2024, 3, 15) + ) mock_get.assert_called_once_with( f"{self.client.BASE_URL}/v2/usercollection/enhanced_tag", @@ -1149,6 +1309,7 @@ def test_get_enhanced_tag_documents(self, mock_get): "end_date": end_date_str, "next_token": "test_enhanced_tag_token", }, + ) @patch("requests.get") @@ -1178,6 +1339,7 @@ def test_get_enhanced_tag_documents_with_string_dates(self, mock_get): f"{self.client.BASE_URL}/v2/usercollection/enhanced_tag", headers=self.client.headers, params={"start_date": start_date_str, "end_date": end_date_str}, + ) @patch("requests.get") @@ -1205,19 +1367,27 @@ def test_get_enhanced_tag_document(self, mock_get): mock_get.return_value = mock_response document_id = "test_enhanced_tag_single" - enhanced_tag_document = self.client.enhanced_tag.get_enhanced_tag_document(document_id=document_id) + enhanced_tag_document = self.client.enhanced_tag.get_enhanced_tag_document( + document_id=document_id + ) self.assertIsInstance(enhanced_tag_document, EnhancedTagModel) self.assertEqual(enhanced_tag_document.id, document_id) self.assertEqual(enhanced_tag_document.tag_type_code, "stress") self.assertEqual(enhanced_tag_document.comment, "Tough day at work.") - self.assertEqual(enhanced_tag_document.start_time, datetime.fromisoformat("2024-03-10T10:00:00+00:00")) - self.assertEqual(enhanced_tag_document.end_day, date(2024, 3, 10)) + self.assertEqual( + enhanced_tag_document.start_time, + datetime.fromisoformat("2024-03-10T10:00:00+00:00") + ) + self.assertEqual( + enhanced_tag_document.end_day, date(2024, 3, 10) + ) mock_get.assert_called_once_with( f"{self.client.BASE_URL}/v2/usercollection/enhanced_tag/{document_id}", headers=self.client.headers, params=None, + ) @patch("requests.get") @@ -1227,9 +1397,9 @@ def test_get_enhanced_tag_document_error(self, mock_get): with self.assertRaises(RequestException): self.client.enhanced_tag.get_enhanced_tag_document(document_id=document_id) - class TestDailySpo2(unittest.TestCase): def setUp(self): + self.client = OuraClient(access_token="test_token") @patch("requests.get") @@ -1249,7 +1419,10 @@ def test_get_daily_spo2_documents(self, mock_get): "timestamp": "2024-03-12T00:00:00+00:00" }, ] - mock_response_json = {"data": mock_data, "next_token": "next_spo2_token"} + mock_response_json = { + "data": mock_data, + "next_token": "next_spo2_token" + } mock_response = MagicMock() mock_response.raise_for_status.return_value = None mock_response.json.return_value = mock_response_json @@ -1261,14 +1434,20 @@ def test_get_daily_spo2_documents(self, mock_get): end_date = date.fromisoformat(end_date_str) daily_spo2_response = self.client.daily_spo2.get_daily_spo2_documents( - start_date=start_date, end_date=end_date, next_token="test_spo2_token" + start_date=start_date, + end_date=end_date, + next_token="test_spo2_token" ) self.assertIsInstance(daily_spo2_response, DailySpO2Response) self.assertEqual(len(daily_spo2_response.data), 2) self.assertIsInstance(daily_spo2_response.data[0], DailySpO2Model) - if daily_spo2_response.data[0].aggregated_values: # Check if aggregated_values exists - self.assertIsInstance(daily_spo2_response.data[0].aggregated_values, DailySpO2AggregatedValuesModel) + # Check if aggregated_values exists + if daily_spo2_response.data[0].aggregated_values: + self.assertIsInstance( + daily_spo2_response.data[0].aggregated_values, + DailySpO2AggregatedValuesModel + ) self.assertEqual(daily_spo2_response.next_token, "next_spo2_token") self.assertEqual(daily_spo2_response.data[0].spo2_percentage, 97.5) @@ -1280,6 +1459,7 @@ def test_get_daily_spo2_documents(self, mock_get): "end_date": end_date_str, "next_token": "test_spo2_token", }, + ) @patch("requests.get") @@ -1309,6 +1489,7 @@ def test_get_daily_spo2_documents_with_string_dates(self, mock_get): f"{self.client.BASE_URL}/v2/usercollection/daily_spo2", headers=self.client.headers, params={"start_date": start_date_str, "end_date": end_date_str}, + ) @patch("requests.get") @@ -1334,19 +1515,25 @@ def test_get_daily_spo2_document(self, mock_get): mock_get.return_value = mock_response document_id = "test_spo2_single" - daily_spo2_document = self.client.daily_spo2.get_daily_spo2_document(document_id=document_id) + daily_spo2_document = self.client.daily_spo2.get_daily_spo2_document( + document_id=document_id + ) self.assertIsInstance(daily_spo2_document, DailySpO2Model) self.assertEqual(daily_spo2_document.id, document_id) self.assertEqual(daily_spo2_document.spo2_percentage, 98.2) if daily_spo2_document.aggregated_values: self.assertEqual(daily_spo2_document.aggregated_values.average, 98.2) - self.assertEqual(daily_spo2_document.timestamp, datetime.fromisoformat("2024-03-11T00:00:00+00:00")) + self.assertEqual( + daily_spo2_document.timestamp, + datetime.fromisoformat("2024-03-11T00:00:00+00:00") + ) mock_get.assert_called_once_with( f"{self.client.BASE_URL}/v2/usercollection/daily_spo2/{document_id}", headers=self.client.headers, params=None, + ) @patch("requests.get") @@ -1356,9 +1543,9 @@ def test_get_daily_spo2_document_error(self, mock_get): with self.assertRaises(RequestException): self.client.daily_spo2.get_daily_spo2_document(document_id=document_id) - class TestSleepTime(unittest.TestCase): def setUp(self): + self.client = OuraClient(access_token="test_token") @patch("requests.get") @@ -1367,7 +1554,11 @@ def test_get_sleep_time_documents(self, mock_get): { "id": "st_1", "day": "2024-03-10", - "optimal_bedtime": {"start_offset": -1800, "end_offset": 3600, "day_light_saving_time": 0}, + "optimal_bedtime": { + "start_offset": -1800, + "end_offset": 3600, + "day_light_saving_time": 0 + }, "recommendation": {"recommendation": "go_to_bed_earlier"}, "status": {"status": "slightly_late"}, "timestamp": "2024-03-10T04:00:00+00:00" @@ -1375,13 +1566,22 @@ def test_get_sleep_time_documents(self, mock_get): { "id": "st_2", "day": "2024-03-11", - "optimal_bedtime": {"start_offset": -1500, "end_offset": 3900}, # Missing day_light_saving_time to test Optional - "recommendation": {"recommendation": "maintain_consistent_schedule"}, + # Missing day_light_saving_time to test Optional + "optimal_bedtime": { + "start_offset": -1500, + "end_offset": 3900 + }, + "recommendation": { + "recommendation": "maintain_consistent_schedule" + }, "status": {"status": "optimal"}, "timestamp": "2024-03-11T04:00:00+00:00" }, ] - mock_response_json = {"data": mock_data, "next_token": "next_sleep_time_token"} + mock_response_json = { + "data": mock_data, + "next_token": "next_sleep_time_token" + } mock_response = MagicMock() mock_response.raise_for_status.return_value = None mock_response.json.return_value = mock_response_json @@ -1393,18 +1593,29 @@ def test_get_sleep_time_documents(self, mock_get): end_date = date.fromisoformat(end_date_str) sleep_time_response = self.client.sleep_time.get_sleep_time_documents( - start_date=start_date, end_date=end_date, next_token="test_sleep_time_token" + start_date=start_date, + end_date=end_date, + next_token="test_sleep_time_token" ) self.assertIsInstance(sleep_time_response, SleepTimeResponse) self.assertEqual(len(sleep_time_response.data), 2) self.assertIsInstance(sleep_time_response.data[0], SleepTimeModel) if sleep_time_response.data[0].optimal_bedtime: - self.assertIsInstance(sleep_time_response.data[0].optimal_bedtime, SleepTimeWindow) + self.assertIsInstance( + sleep_time_response.data[0].optimal_bedtime, + SleepTimeWindow + ) if sleep_time_response.data[0].recommendation: - self.assertIsInstance(sleep_time_response.data[0].recommendation, SleepTimeRecommendation) + self.assertIsInstance( + sleep_time_response.data[0].recommendation, + SleepTimeRecommendation + ) if sleep_time_response.data[0].status: - self.assertIsInstance(sleep_time_response.data[0].status, SleepTimeStatus) + self.assertIsInstance( + sleep_time_response.data[0].status, + SleepTimeStatus + ) self.assertEqual(sleep_time_response.next_token, "next_sleep_time_token") self.assertEqual(sleep_time_response.data[0].day, date(2024, 3, 10)) @@ -1416,6 +1627,7 @@ def test_get_sleep_time_documents(self, mock_get): "end_date": end_date_str, "next_token": "test_sleep_time_token", }, + ) @patch("requests.get") @@ -1445,6 +1657,7 @@ def test_get_sleep_time_documents_with_string_dates(self, mock_get): f"{self.client.BASE_URL}/v2/usercollection/sleep_time", headers=self.client.headers, params={"start_date": start_date_str, "end_date": end_date_str}, + ) @patch("requests.get") @@ -1460,7 +1673,11 @@ def test_get_sleep_time_document(self, mock_get): mock_response_json = { "id": "test_st_single", "day": "2024-03-10", - "optimal_bedtime": {"start_offset": -1800, "end_offset": 3600, "day_light_saving_time": 0}, + "optimal_bedtime": { + "start_offset": -1800, + "end_offset": 3600, + "day_light_saving_time": 0 + }, "recommendation": {"recommendation": "go_to_bed_earlier"}, "status": {"status": "slightly_late"}, "timestamp": "2024-03-10T04:00:00+00:00" @@ -1471,36 +1688,50 @@ def test_get_sleep_time_document(self, mock_get): mock_get.return_value = mock_response document_id = "test_st_single" - sleep_time_document = self.client.sleep_time.get_sleep_time_document(document_id=document_id) + sleep_time_document = self.client.sleep_time.get_sleep_time_document( + document_id=document_id + ) self.assertIsInstance(sleep_time_document, SleepTimeModel) self.assertEqual(sleep_time_document.id, document_id) if sleep_time_document.optimal_bedtime: - self.assertEqual(sleep_time_document.optimal_bedtime.start_offset, -1800) + self.assertEqual( + sleep_time_document.optimal_bedtime.start_offset, -1800 + ) if sleep_time_document.recommendation: - self.assertEqual(sleep_time_document.recommendation.recommendation, "go_to_bed_earlier") + self.assertEqual( + sleep_time_document.recommendation.recommendation, + "go_to_bed_earlier" + ) if sleep_time_document.status: - self.assertEqual(sleep_time_document.status.status, "slightly_late") - self.assertEqual(sleep_time_document.timestamp, datetime.fromisoformat("2024-03-10T04:00:00+00:00")) + self.assertEqual( + sleep_time_document.status.status, "slightly_late" + ) + self.assertEqual( + sleep_time_document.timestamp, + datetime.fromisoformat("2024-03-10T04:00:00+00:00") + ) mock_get.assert_called_once_with( f"{self.client.BASE_URL}/v2/usercollection/sleep_time/{document_id}", headers=self.client.headers, params=None, + ) @patch("requests.get") def test_get_sleep_time_document_error(self, mock_get): # As per the implementation note, this endpoint might not exist. - # If it doesn't, the API would return a 404, which _make_request would raise as an HTTPError (a subclass of RequestException). + # If it doesn't, the API would return a 404, which _make_request would + # raise as an HTTPError (a subclass of RequestException). mock_get.side_effect = RequestException("API error or Not Found") document_id = "test_st_single_error" with self.assertRaises(RequestException): self.client.sleep_time.get_sleep_time_document(document_id=document_id) - class TestRestModePeriod(unittest.TestCase): def setUp(self): + self.client = OuraClient(access_token="test_token") @patch("requests.get") @@ -1533,15 +1764,23 @@ def test_get_rest_mode_period_documents(self, mock_get): start_date = date.fromisoformat(start_date_str) end_date = date.fromisoformat(end_date_str) - rest_mode_response = self.client.rest_mode_period.get_rest_mode_period_documents( - start_date=start_date, end_date=end_date, next_token="test_rmp_token" + rest_mode_response = ( + self.client.rest_mode_period.get_rest_mode_period_documents( + start_date=start_date, + end_date=end_date, + next_token="test_rmp_token" + ) ) self.assertIsInstance(rest_mode_response, RestModePeriodResponse) self.assertEqual(len(rest_mode_response.data), 2) - self.assertIsInstance(rest_mode_response.data[0], RestModePeriodModel) + self.assertIsInstance( + rest_mode_response.data[0], RestModePeriodModel + ) self.assertEqual(rest_mode_response.next_token, "next_rmp_token") - self.assertEqual(rest_mode_response.data[0].rest_mode_state, "on_demand_rest") + self.assertEqual( + rest_mode_response.data[0].rest_mode_state, "on_demand_rest" + ) mock_get.assert_called_once_with( f"{self.client.BASE_URL}/v2/usercollection/rest_mode_period", @@ -1551,6 +1790,7 @@ def test_get_rest_mode_period_documents(self, mock_get): "end_date": end_date_str, "next_token": "test_rmp_token", }, + ) @patch("requests.get") @@ -1580,6 +1820,7 @@ def test_get_rest_mode_period_documents_with_string_dates(self, mock_get): f"{self.client.BASE_URL}/v2/usercollection/rest_mode_period", headers=self.client.headers, params={"start_date": start_date_str, "end_date": end_date_str}, + ) @patch("requests.get") @@ -1608,18 +1849,26 @@ def test_get_rest_mode_period_document(self, mock_get): mock_get.return_value = mock_response document_id = "test_rmp_single" - rmp_document = self.client.rest_mode_period.get_rest_mode_period_document(document_id=document_id) + rmp_document = self.client.rest_mode_period.get_rest_mode_period_document( + document_id=document_id + ) self.assertIsInstance(rmp_document, RestModePeriodModel) self.assertEqual(rmp_document.id, document_id) - self.assertEqual(rmp_document.rest_mode_state, "recovering_from_illness") + self.assertEqual( + rmp_document.rest_mode_state, "recovering_from_illness" + ) self.assertEqual(rmp_document.baseline_hrv, 48) - self.assertEqual(rmp_document.start_time, datetime.fromisoformat("2024-03-10T10:00:00+00:00")) + self.assertEqual( + rmp_document.start_time, + datetime.fromisoformat("2024-03-10T10:00:00+00:00") + ) mock_get.assert_called_once_with( f"{self.client.BASE_URL}/v2/usercollection/rest_mode_period/{document_id}", headers=self.client.headers, params=None, + ) @patch("requests.get") @@ -1627,11 +1876,13 @@ def test_get_rest_mode_period_document_error(self, mock_get): mock_get.side_effect = RequestException("API error") document_id = "test_rmp_single_error" with self.assertRaises(RequestException): - self.client.rest_mode_period.get_rest_mode_period_document(document_id=document_id) - + self.client.rest_mode_period.get_rest_mode_period_document( + document_id=document_id + ) class TestDailyStress(unittest.TestCase): def setUp(self): + self.client = OuraClient(access_token="test_token") self.base_url = self.client.BASE_URL @@ -1659,7 +1910,9 @@ def test_get_daily_stress_documents_start_date(self, mock_get): mock_response.json.return_value = mock_response_data mock_get.return_value = mock_response - self.client.daily_stress.get_daily_stress_documents(start_date="2024-01-01") + self.client.daily_stress.get_daily_stress_documents( + start_date="2024-01-01" + ) mock_get.assert_called_once_with( f"{self.base_url}/v2/usercollection/daily_stress", headers=self.client.headers, @@ -1689,7 +1942,9 @@ def test_get_daily_stress_documents_start_and_end_date(self, mock_get): mock_response.json.return_value = mock_response_data mock_get.return_value = mock_response - self.client.daily_stress.get_daily_stress_documents(start_date="2024-01-01", end_date="2024-01-31") + self.client.daily_stress.get_daily_stress_documents( + start_date="2024-01-01", end_date="2024-01-31" + ) mock_get.assert_called_once_with( f"{self.base_url}/v2/usercollection/daily_stress", headers=self.client.headers, @@ -1704,7 +1959,9 @@ def test_get_daily_stress_documents_next_token(self, mock_get): mock_response.json.return_value = mock_response_data mock_get.return_value = mock_response - self.client.daily_stress.get_daily_stress_documents(next_token="some_token") + self.client.daily_stress.get_daily_stress_documents( + next_token="some_token" + ) mock_get.assert_called_once_with( f"{self.base_url}/v2/usercollection/daily_stress", headers=self.client.headers, @@ -1720,21 +1977,28 @@ def test_get_daily_stress_documents_success(self, mock_get): "stress_high": 1200, "recovery_high": 3600, "day_summary": "restored", + "timestamp": "2024-03-15T08:00:00Z", } ] - mock_response_json = {"data": mock_data, "next_token": "stress_next_token"} + mock_response_json = { + "data": mock_data, + "next_token": "stress_next_token" + } mock_response = MagicMock() mock_response.raise_for_status.return_value = None mock_response.json.return_value = mock_response_json mock_get.return_value = mock_response - response = self.client.daily_stress.get_daily_stress_documents(start_date="2024-03-15") + response = self.client.daily_stress.get_daily_stress_documents( + start_date="2024-03-15" + ) self.assertIsInstance(response, DailyStressResponse) self.assertEqual(len(response.data), 1) self.assertIsInstance(response.data[0], DailyStressModel) self.assertEqual(response.data[0].id, "stress_doc_1") self.assertEqual(response.data[0].stress_high, 1200) - self.assertEqual(response.data[0].day_summary, DailyStressSummary.RESTORED) # Assuming DailyStressSummary is an Enum + # Check day_summary value + self.assertEqual(response.data[0].day_summary, "restored") self.assertEqual(response.next_token, "stress_next_token") mock_get.assert_called_with( f"{self.base_url}/v2/usercollection/daily_stress", @@ -1745,7 +2009,9 @@ def test_get_daily_stress_documents_success(self, mock_get): @patch("requests.get") def test_get_daily_stress_documents_api_error_400(self, mock_get): mock_response = MagicMock() - mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("400 Client Error") + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("400 Client Error") + ) mock_get.return_value = mock_response with self.assertRaises(requests.exceptions.HTTPError): self.client.daily_stress.get_daily_stress_documents() @@ -1753,7 +2019,9 @@ def test_get_daily_stress_documents_api_error_400(self, mock_get): @patch("requests.get") def test_get_daily_stress_documents_api_error_401(self, mock_get): mock_response = MagicMock() - mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("401 Client Error") + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("401 Client Error") + ) mock_get.return_value = mock_response with self.assertRaises(requests.exceptions.HTTPError): self.client.daily_stress.get_daily_stress_documents() @@ -1761,7 +2029,9 @@ def test_get_daily_stress_documents_api_error_401(self, mock_get): @patch("requests.get") def test_get_daily_stress_documents_api_error_429(self, mock_get): mock_response = MagicMock() - mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("429 Client Error") + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("429 Client Error") + ) mock_get.return_value = mock_response with self.assertRaises(requests.exceptions.HTTPError): self.client.daily_stress.get_daily_stress_documents() @@ -1775,6 +2045,7 @@ def test_get_daily_stress_document_success(self, mock_get): "stress_high": 1500, "recovery_high": 3000, "day_summary": "stressful", + "timestamp": "2024-03-16T08:00:00Z", } mock_response = MagicMock() mock_response.raise_for_status.return_value = None @@ -1786,27 +2057,30 @@ def test_get_daily_stress_document_success(self, mock_get): self.assertEqual(response.id, document_id) self.assertEqual(response.day, date(2024, 3, 16)) self.assertEqual(response.stress_high, 1500) - self.assertEqual(response.day_summary, DailyStressSummary.STRESSFUL) # Assuming DailyStressSummary is an Enum + # Check day_summary value + self.assertEqual(response.day_summary, "stressful") mock_get.assert_called_once_with( f"{self.base_url}/v2/usercollection/daily_stress/{document_id}", headers=self.client.headers, - params=None, # No params for single document GET + params=None, # No params for single document GET ) @patch("requests.get") def test_get_daily_stress_document_not_found_404(self, mock_get): document_id = "non_existent_id" mock_response = MagicMock() - mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("404 Client Error: Not Found") + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("404 Client Error: Not Found") + ) mock_get.return_value = mock_response with self.assertRaises(requests.exceptions.HTTPError): self.client.daily_stress.get_daily_stress_document(document_id) - class TestDailyResilience(unittest.TestCase): def setUp(self): + self.client = OuraClient(access_token="test_token") self.base_url = self.client.BASE_URL @@ -1834,7 +2108,9 @@ def test_get_daily_resilience_documents_start_date(self, mock_get): mock_response.json.return_value = mock_response_data mock_get.return_value = mock_response - self.client.daily_resilience.get_daily_resilience_documents(start_date="2024-02-01") + self.client.daily_resilience.get_daily_resilience_documents( + start_date="2024-02-01" + ) mock_get.assert_called_once_with( f"{self.base_url}/v2/usercollection/daily_resilience", headers=self.client.headers, @@ -1849,7 +2125,9 @@ def test_get_daily_resilience_documents_end_date(self, mock_get): mock_response.json.return_value = mock_response_data mock_get.return_value = mock_response - self.client.daily_resilience.get_daily_resilience_documents(end_date="2024-02-28") + self.client.daily_resilience.get_daily_resilience_documents( + end_date="2024-02-28" + ) mock_get.assert_called_once_with( f"{self.base_url}/v2/usercollection/daily_resilience", headers=self.client.headers, @@ -1864,7 +2142,9 @@ def test_get_daily_resilience_documents_start_and_end_date(self, mock_get): mock_response.json.return_value = mock_response_data mock_get.return_value = mock_response - self.client.daily_resilience.get_daily_resilience_documents(start_date="2024-02-01", end_date="2024-02-28") + self.client.daily_resilience.get_daily_resilience_documents( + start_date="2024-02-01", end_date="2024-02-28" + ) mock_get.assert_called_once_with( f"{self.base_url}/v2/usercollection/daily_resilience", headers=self.client.headers, @@ -1879,7 +2159,9 @@ def test_get_daily_resilience_documents_next_token(self, mock_get): mock_response.json.return_value = mock_response_data mock_get.return_value = mock_response - self.client.daily_resilience.get_daily_resilience_documents(next_token="res_token") + self.client.daily_resilience.get_daily_resilience_documents( + next_token="res_token" + ) mock_get.assert_called_once_with( f"{self.base_url}/v2/usercollection/daily_resilience", headers=self.client.headers, @@ -1899,23 +2181,30 @@ def test_get_daily_resilience_documents_success(self, mock_get): "day": "2024-03-18", "contributors": mock_contributors_data, "level": "solid", + "timestamp": "2024-03-18T08:00:00Z", } ] - mock_response_json = {"data": mock_data, "next_token": "res_next_token"} + mock_response_json = { + "data": mock_data, + "next_token": "res_next_token" + } mock_response = MagicMock() mock_response.raise_for_status.return_value = None mock_response.json.return_value = mock_response_json mock_get.return_value = mock_response - response = self.client.daily_resilience.get_daily_resilience_documents(start_date="2024-03-18") + response = self.client.daily_resilience.get_daily_resilience_documents( + start_date="2024-03-18" + ) self.assertIsInstance(response, DailyResilienceResponse) self.assertEqual(len(response.data), 1) model_item = response.data[0] self.assertIsInstance(model_item, DailyResilienceModel) self.assertEqual(model_item.id, "res_doc_1") - self.assertIsInstance(model_item.contributors, ResilienceContributors) + # Check contributors + self.assertIsNotNone(model_item.contributors) self.assertEqual(model_item.contributors.sleep_recovery, 75.0) - self.assertEqual(model_item.level, LongTermResilienceLevel.SOLID) + self.assertEqual(model_item.level, "solid") self.assertEqual(response.next_token, "res_next_token") mock_get.assert_called_with( f"{self.base_url}/v2/usercollection/daily_resilience", @@ -1926,7 +2215,9 @@ def test_get_daily_resilience_documents_success(self, mock_get): @patch("requests.get") def test_get_daily_resilience_documents_api_error_400(self, mock_get): mock_response = MagicMock() - mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("400 Client Error") + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("400 Client Error") + ) mock_get.return_value = mock_response with self.assertRaises(requests.exceptions.HTTPError): self.client.daily_resilience.get_daily_resilience_documents() @@ -1934,7 +2225,9 @@ def test_get_daily_resilience_documents_api_error_400(self, mock_get): @patch("requests.get") def test_get_daily_resilience_documents_api_error_401(self, mock_get): mock_response = MagicMock() - mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("401 Client Error") + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("401 Client Error") + ) mock_get.return_value = mock_response with self.assertRaises(requests.exceptions.HTTPError): self.client.daily_resilience.get_daily_resilience_documents() @@ -1942,7 +2235,9 @@ def test_get_daily_resilience_documents_api_error_401(self, mock_get): @patch("requests.get") def test_get_daily_resilience_documents_api_error_429(self, mock_get): mock_response = MagicMock() - mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("429 Client Error") + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("429 Client Error") + ) mock_get.return_value = mock_response with self.assertRaises(requests.exceptions.HTTPError): self.client.daily_resilience.get_daily_resilience_documents() @@ -1960,19 +2255,23 @@ def test_get_daily_resilience_document_success(self, mock_get): "day": "2024-03-19", "contributors": mock_contributors_data, "level": "exceptional", + "timestamp": "2024-03-19T08:00:00Z", } mock_response = MagicMock() mock_response.raise_for_status.return_value = None mock_response.json.return_value = mock_response_json mock_get.return_value = mock_response - response = self.client.daily_resilience.get_daily_resilience_document(document_id) + response = self.client.daily_resilience.get_daily_resilience_document( + document_id + ) self.assertIsInstance(response, DailyResilienceModel) self.assertEqual(response.id, document_id) self.assertEqual(response.day, date(2024, 3, 19)) - self.assertIsInstance(response.contributors, ResilienceContributors) + # Check contributors + self.assertIsNotNone(response.contributors) self.assertEqual(response.contributors.daytime_recovery, 65.2) - self.assertEqual(response.level, LongTermResilienceLevel.EXCEPTIONAL) + self.assertEqual(response.level, "exceptional") mock_get.assert_called_once_with( f"{self.base_url}/v2/usercollection/daily_resilience/{document_id}", @@ -1984,15 +2283,19 @@ def test_get_daily_resilience_document_success(self, mock_get): def test_get_daily_resilience_document_not_found_404(self, mock_get): document_id = "non_existent_res_id" mock_response = MagicMock() - mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("404 Client Error: Not Found") + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("404 Client Error: Not Found") + ) mock_get.return_value = mock_response with self.assertRaises(requests.exceptions.HTTPError): - self.client.daily_resilience.get_daily_resilience_document(document_id) - + self.client.daily_resilience.get_daily_resilience_document( + document_id + ) class TestDailyCardiovascularAge(unittest.TestCase): def setUp(self): + self.client = OuraClient(access_token="test_token") self.base_url = self.client.BASE_URL @@ -2004,7 +2307,9 @@ def test_get_daily_cardiovascular_age_documents_no_params(self, mock_get): mock_response.json.return_value = mock_response_data mock_get.return_value = mock_response - response = self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_documents() + response = ( + self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_documents() + ) self.assertIsInstance(response, DailyCardiovascularAgeResponse) mock_get.assert_called_once_with( f"{self.base_url}/v2/usercollection/daily_cardiovascular_age", @@ -2020,7 +2325,9 @@ def test_get_daily_cardiovascular_age_documents_start_date(self, mock_get): mock_response.json.return_value = mock_response_data mock_get.return_value = mock_response - self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_documents(start_date="2024-03-01") + self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_documents( + start_date="2024-03-01" + ) mock_get.assert_called_once_with( f"{self.base_url}/v2/usercollection/daily_cardiovascular_age", headers=self.client.headers, @@ -2035,7 +2342,9 @@ def test_get_daily_cardiovascular_age_documents_end_date(self, mock_get): mock_response.json.return_value = mock_response_data mock_get.return_value = mock_response - self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_documents(end_date="2024-03-31") + self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_documents( + end_date="2024-03-31" + ) mock_get.assert_called_once_with( f"{self.base_url}/v2/usercollection/daily_cardiovascular_age", headers=self.client.headers, @@ -2050,7 +2359,9 @@ def test_get_daily_cardiovascular_age_documents_start_and_end_date(self, mock_ge mock_response.json.return_value = mock_response_data mock_get.return_value = mock_response - self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_documents(start_date="2024-03-01", end_date="2024-03-31") + self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_documents( + start_date="2024-03-01", end_date="2024-03-31" + ) mock_get.assert_called_once_with( f"{self.base_url}/v2/usercollection/daily_cardiovascular_age", headers=self.client.headers, @@ -2065,7 +2376,9 @@ def test_get_daily_cardiovascular_age_documents_next_token(self, mock_get): mock_response.json.return_value = mock_response_data mock_get.return_value = mock_response - self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_documents(next_token="cva_token") + self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_documents( + next_token="cva_token" + ) mock_get.assert_called_once_with( f"{self.base_url}/v2/usercollection/daily_cardiovascular_age", headers=self.client.headers, @@ -2080,16 +2393,24 @@ def test_get_daily_cardiovascular_age_documents_success(self, mock_get): # This is fine, Pydantic will ignore extra fields. "id": "cva_doc_api_id_1", "day": "2024-03-20", - "vascular_age": 30.5, # Changed to float to match spec + "vascular_age": 30.5, # Changed to float to match spec + "timestamp": "2024-03-20T08:00:00Z", } ] - mock_response_json = {"data": mock_data, "next_token": "cva_next_token"} + mock_response_json = { + "data": mock_data, + "next_token": "cva_next_token" + } mock_response = MagicMock() mock_response.raise_for_status.return_value = None mock_response.json.return_value = mock_response_json mock_get.return_value = mock_response - response = self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_documents(start_date="2024-03-20") + response = ( + self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_documents( + start_date="2024-03-20" + ) + ) self.assertIsInstance(response, DailyCardiovascularAgeResponse) self.assertEqual(len(response.data), 1) model_item = response.data[0] @@ -2106,7 +2427,9 @@ def test_get_daily_cardiovascular_age_documents_success(self, mock_get): @patch("requests.get") def test_get_daily_cardiovascular_age_documents_api_error_400(self, mock_get): mock_response = MagicMock() - mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("400 Client Error") + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("400 Client Error") + ) mock_get.return_value = mock_response with self.assertRaises(requests.exceptions.HTTPError): self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_documents() @@ -2114,7 +2437,9 @@ def test_get_daily_cardiovascular_age_documents_api_error_400(self, mock_get): @patch("requests.get") def test_get_daily_cardiovascular_age_documents_api_error_401(self, mock_get): mock_response = MagicMock() - mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("401 Client Error") + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("401 Client Error") + ) mock_get.return_value = mock_response with self.assertRaises(requests.exceptions.HTTPError): self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_documents() @@ -2122,7 +2447,9 @@ def test_get_daily_cardiovascular_age_documents_api_error_401(self, mock_get): @patch("requests.get") def test_get_daily_cardiovascular_age_documents_api_error_429(self, mock_get): mock_response = MagicMock() - mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("429 Client Error") + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("429 Client Error") + ) mock_get.return_value = mock_response with self.assertRaises(requests.exceptions.HTTPError): self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_documents() @@ -2133,14 +2460,19 @@ def test_get_daily_cardiovascular_age_document_success(self, mock_get): mock_response_json = { "id": document_id, "day": "2024-03-21", - "vascular_age": 32.0, # Changed to float + "vascular_age": 32.0, # Changed to float + "timestamp": "2024-03-21T08:00:00Z", } mock_response = MagicMock() mock_response.raise_for_status.return_value = None mock_response.json.return_value = mock_response_json mock_get.return_value = mock_response - response = self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_document(document_id) + response = ( + self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_document( + document_id + ) + ) self.assertIsInstance(response, DailyCardiovascularAgeModel) self.assertEqual(response.day, date(2024, 3, 21)) self.assertEqual(response.vascular_age, 32.0) @@ -2155,18 +2487,22 @@ def test_get_daily_cardiovascular_age_document_success(self, mock_get): def test_get_daily_cardiovascular_age_document_not_found_404(self, mock_get): document_id = "non_existent_cva_id" mock_response = MagicMock() - mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("404 Client Error: Not Found") + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("404 Client Error: Not Found") + ) mock_get.return_value = mock_response with self.assertRaises(requests.exceptions.HTTPError): - self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_document(document_id) - + self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_document( + document_id + ) class TestVo2Max(unittest.TestCase): def setUp(self): + self.client = OuraClient(access_token="test_token") self.base_url = self.client.BASE_URL - self.correct_path_segment = "/v2/usercollection/vO2_max" # Note the casing + self.correct_path_segment = "/v2/usercollection/vO2_max" # Note the casing @patch("requests.get") def test_get_vo2_max_documents_no_params(self, mock_get): @@ -2222,7 +2558,9 @@ def test_get_vo2_max_documents_start_and_end_date(self, mock_get): mock_response.json.return_value = mock_response_data mock_get.return_value = mock_response - self.client.vo2_max.get_vo2_max_documents(start_date="2024-04-01", end_date="2024-04-30") + self.client.vo2_max.get_vo2_max_documents( + start_date="2024-04-01", end_date="2024-04-30" + ) mock_get.assert_called_once_with( f"{self.base_url}{self.correct_path_segment}", headers=self.client.headers, @@ -2254,13 +2592,18 @@ def test_get_vo2_max_documents_success(self, mock_get): "vo2_max": 35.5, } ] - mock_response_json = {"data": mock_data, "next_token": "vo2_next_token"} + mock_response_json = { + "data": mock_data, + "next_token": "vo2_next_token" + } mock_response = MagicMock() mock_response.raise_for_status.return_value = None mock_response.json.return_value = mock_response_json mock_get.return_value = mock_response - response = self.client.vo2_max.get_vo2_max_documents(start_date="2024-04-10") + response = self.client.vo2_max.get_vo2_max_documents( + start_date="2024-04-10" + ) self.assertIsInstance(response, Vo2MaxResponse) self.assertEqual(len(response.data), 1) model_item = response.data[0] @@ -2278,7 +2621,9 @@ def test_get_vo2_max_documents_success(self, mock_get): @patch("requests.get") def test_get_vo2_max_documents_api_error_400(self, mock_get): mock_response = MagicMock() - mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("400 Client Error") + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("400 Client Error") + ) mock_get.return_value = mock_response with self.assertRaises(requests.exceptions.HTTPError): self.client.vo2_max.get_vo2_max_documents() @@ -2286,7 +2631,9 @@ def test_get_vo2_max_documents_api_error_400(self, mock_get): @patch("requests.get") def test_get_vo2_max_documents_api_error_401(self, mock_get): mock_response = MagicMock() - mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("401 Client Error") + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("401 Client Error") + ) mock_get.return_value = mock_response with self.assertRaises(requests.exceptions.HTTPError): self.client.vo2_max.get_vo2_max_documents() @@ -2294,7 +2641,9 @@ def test_get_vo2_max_documents_api_error_401(self, mock_get): @patch("requests.get") def test_get_vo2_max_documents_api_error_429(self, mock_get): mock_response = MagicMock() - mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("429 Client Error") + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("429 Client Error") + ) mock_get.return_value = mock_response with self.assertRaises(requests.exceptions.HTTPError): self.client.vo2_max.get_vo2_max_documents() @@ -2317,7 +2666,10 @@ def test_get_vo2_max_document_success(self, mock_get): self.assertIsInstance(response, Vo2MaxModel) self.assertEqual(response.id, document_id) self.assertEqual(response.day, date(2024, 4, 11)) - self.assertEqual(response.timestamp, datetime.fromisoformat("2024-04-11T11:00:00+00:00")) + self.assertEqual( + response.timestamp, + datetime.fromisoformat("2024-04-11T11:00:00+00:00") + ) self.assertEqual(response.vo2_max, 36.2) mock_get.assert_called_once_with( @@ -2330,7 +2682,9 @@ def test_get_vo2_max_document_success(self, mock_get): def test_get_vo2_max_document_not_found_404(self, mock_get): document_id = "non_existent_vo2_id" mock_response = MagicMock() - mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("404 Client Error: Not Found") + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("404 Client Error: Not Found") + ) mock_get.return_value = mock_response with self.assertRaises(requests.exceptions.HTTPError): diff --git a/tests/test_client.py.backup b/tests/test_client.py.backup new file mode 100644 index 0000000..914bd72 --- /dev/null +++ b/tests/test_client.py.backup @@ -0,0 +1,2745 @@ +"""Tests for the Oura API client.""" + +import unittest +from unittest.mock import patch, MagicMock +from datetime import datetime, date + +from oura_api_client.api.client import OuraClient +from oura_api_client.models.heartrate import HeartRateResponse +from oura_api_client.models.daily_activity import ( + DailyActivityResponse, DailyActivityModel, ActivityContributors +) +from oura_api_client.models.daily_sleep import ( + DailySleepResponse, DailySleepModel, SleepContributors +) +from oura_api_client.models.daily_readiness import ( + DailyReadinessResponse, DailyReadinessModel, ReadinessContributors +) +from oura_api_client.models.sleep import SleepResponse, SleepModel +from oura_api_client.models.session import SessionResponse, SessionModel +from oura_api_client.models.tag import TagResponse, TagModel +from oura_api_client.models.workout import WorkoutResponse, WorkoutModel +from oura_api_client.models.enhanced_tag import ( + EnhancedTagResponse, EnhancedTagModel +) +from oura_api_client.models.daily_spo2 import ( + DailySpO2Response, DailySpO2Model, DailySpO2AggregatedValuesModel +) +from oura_api_client.models.sleep_time import ( + SleepTimeResponse, SleepTimeModel, SleepTimeWindow, + SleepTimeRecommendation, SleepTimeStatus +) +from oura_api_client.models.rest_mode_period import ( + RestModePeriodResponse, RestModePeriodModel +) # Added RestModePeriod models +from oura_api_client.models.daily_stress import ( + DailyStressResponse, DailyStressModel +) # Added DailyStress models +from oura_api_client.models.daily_resilience import ( + DailyResilienceResponse, DailyResilienceModel +) # Added DailyResilience models +from oura_api_client.models.daily_cardiovascular_age import ( + DailyCardiovascularAgeResponse, DailyCardiovascularAgeModel +) # Added DailyCardiovascularAge models +from oura_api_client.models.vo2_max import ( + Vo2MaxResponse, Vo2MaxModel +) # Added Vo2Max models +import requests +from requests.exceptions import RequestException + +class TestOuraClient(unittest.TestCase): + """Test the OuraClient class.""" + + def setUp(self): + """Set up a client for testing.""" + self.client = OuraClient("test_token") + + def test_initialization(self): + """Test that the client initializes correctly.""" + self.assertEqual(self.client.access_token, "test_token") + self.assertEqual( + self.client.headers["Authorization"], "Bearer test_token" + ) + self.assertIsNotNone(self.client.heartrate) + self.assertIsNotNone(self.client.personal) + self.assertIsNotNone(self.client.daily_activity) + self.assertIsNotNone(self.client.daily_sleep) + self.assertIsNotNone(self.client.daily_readiness) + self.assertIsNotNone(self.client.sleep) + self.assertIsNotNone(self.client.session) + self.assertIsNotNone(self.client.tag) + self.assertIsNotNone(self.client.workout) + self.assertIsNotNone(self.client.enhanced_tag) + self.assertIsNotNone(self.client.daily_spo2) + self.assertIsNotNone(self.client.sleep_time) + # Added rest_mode_period + self.assertIsNotNone(self.client.rest_mode_period) + # Added daily_stress + self.assertIsNotNone(self.client.daily_stress) + # Added daily_resilience + self.assertIsNotNone(self.client.daily_resilience) + # Added daily_cardiovascular_age + self.assertIsNotNone(self.client.daily_cardiovascular_age) + # Added vo2_max + self.assertIsNotNone(self.client.vo2_max) + + @patch("requests.get") + def test_get_heart_rate(self, mock_get): + """Test getting heart rate data.""" + # Mock the API response + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = { + "data": [ + { + "timestamp": "2024-03-01T12:00:00+00:00", + "bpm": 75, + "source": "test" + } + ], + "next_token": None, + } + mock_get.return_value = mock_response + # Test with return_model=True (default) + heart_rate = self.client.heartrate.get_heartrate( + start_date="2024-03-01", end_date="2024-03-15" + ) + # Assert that the response was properly converted to a model + self.assertIsInstance(heart_rate, HeartRateResponse) + self.assertEqual(len(heart_rate.data), 1) + self.assertEqual(heart_rate.data[0].bpm, 75) + # Test with return_model=False + heart_rate_raw = self.client.heartrate.get_heartrate( + start_date="2024-03-01", end_date="2024-03-15", return_model=False + ) + # Assert that the raw response was returned + self.assertIsInstance(heart_rate_raw, dict) + self.assertIn("data", heart_rate_raw) + # Verify the API was called with the correct parameters + mock_get.assert_called_with( + "https://api.ouraring.com/v2/usercollection/heartrate", + headers=self.client.headers, + params={"start_date": "2024-03-01", "end_date": "2024-03-15"}, + ) + +if __name__ == "__main__": + unittest.main() + +class TestDailyActivity(unittest.TestCase): + def setUp(self): + + self.client = OuraClient(access_token="test_token") + + @patch("requests.get") + def test_get_daily_activity_documents(self, mock_get): + mock_data = [ + { + "id": "test_id_1", + "day": "2024-03-10", + "timestamp": "2024-03-10T00:00:00+00:00", + }, + { + "id": "test_id_2", + "day": "2024-03-11", + "timestamp": "2024-03-11T00:00:00+00:00", + }, + ] + mock_response_json = { + "data": mock_data, + "next_token": "test_next_token" + } + # Configure the mock_get object to simulate a successful response + mock_response = MagicMock() +<<<<<<< HEAD + mock_response.raise_for_status.return_value = None # Simulate no HTTP error + mock_response.json.return_value = mock_response_json # Set the JSON response +======= + # Simulate no HTTP error + mock_response.raise_for_status.return_value = None + # Set the JSON response + mock_response.json.return_value = mock_response_json +>>>>>>> cd7b1320f6e9ecc96b943f9eaa71c4a664f66e3f + mock_get.return_value = mock_response + + start_date_str = "2024-03-10" + end_date_str = "2024-03-11" + start_date = date.fromisoformat(start_date_str) + end_date = date.fromisoformat(end_date_str) + + daily_activity_response = ( + self.client.daily_activity.get_daily_activity_documents( + start_date=start_date, + end_date=end_date, + next_token="test_token" + ) + ) + + self.assertIsInstance(daily_activity_response, DailyActivityResponse) + self.assertEqual(len(daily_activity_response.data), 2) + self.assertIsInstance( + daily_activity_response.data[0], DailyActivityModel + ) + self.assertEqual( + daily_activity_response.next_token, "test_next_token" + ) + # Use self.client.client._make_request for assertion + # if client.get is not available + actual_call_url = mock_get.call_args[0][0] + base_url = self.client.BASE_URL + expected_url = f"{base_url}/v2/usercollection/daily_activity" + self.assertTrue(actual_call_url.endswith(expected_url)) + + called_params = mock_get.call_args[1]['params'] + expected_params = { + "start_date": start_date_str, + "end_date": end_date_str, + "next_token": "test_token", + } + self.assertEqual(called_params, expected_params) + + @patch("requests.get") + def test_get_daily_activity_documents_with_string_dates(self, mock_get): + mock_data = [ + { + "id": "test_id_1", + "day": "2024-03-10", + "timestamp": "2024-03-10T00:00:00+00:00", + } + ] + mock_response_json = {"data": mock_data, "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + start_date_str = "2024-03-10" + end_date_str = "2024-03-11" + + self.client.daily_activity.get_daily_activity_documents( + start_date=start_date_str, end_date=end_date_str + ) + actual_call_url = mock_get.call_args[0][0] + base_url = self.client.BASE_URL + expected_url = f"{base_url}/v2/usercollection/daily_activity" + self.assertTrue(actual_call_url.endswith(expected_url)) + + called_params = mock_get.call_args[1]['params'] + expected_params = {"start_date": start_date_str, "end_date": end_date_str} + self.assertEqual(called_params, expected_params) + + @patch("requests.get") + def test_get_daily_activity_documents_error(self, mock_get): + mock_get.side_effect = RequestException("API error") + with self.assertRaises(RequestException): + self.client.daily_activity.get_daily_activity_documents( + start_date="2024-03-10", end_date="2024-03-11" + ) + + @patch("requests.get") + def test_get_daily_activity_document(self, mock_get): + mock_response_json = { + "id": "test_document_id", + "class_5_min": "test_class_5_min", + "score": 80, + "active_calories": 500, + "average_met_minutes": 2.5, + "contributors": { + "meet_daily_targets": 1, + "move_every_hour": 1, + "recovery_time": 1, + "stay_active": 1, + "training_frequency": 1, + "training_volume": 1, + }, + "equivalent_walking_distance": 5000, + "high_activity_met_minutes": 30, + "high_activity_time": 600, + "inactivity_alerts": 2, + "low_activity_met_minutes": 60, + "low_activity_time": 1200, + "medium_activity_met_minutes": 90, + "medium_activity_time": 1800, + "met": "test_met", + "meters_to_target": 1000, + "non_wear_time": 300, + "resting_time": 3600, + "sedentary_met_minutes": 120, + "sedentary_time": 2400, + "steps": 10000, + "target_calories": 600, + "target_meters": 6000, + "total_calories": 2500, + "day": "2024-03-10", + "timestamp": "2024-03-10T12:00:00+00:00", + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + document_id = "test_document_id" + daily_activity_document = self.client.daily_activity.get_daily_activity_document( + document_id=document_id + ) + + self.assertIsInstance(daily_activity_document, DailyActivityModel) + self.assertEqual(daily_activity_document.id, document_id) + self.assertIsInstance(daily_activity_document.contributors, ActivityContributors) + + actual_call_url = mock_get.call_args[0][0] + base_url = self.client.BASE_URL + expected_url = f"{base_url}/v2/usercollection/daily_activity/{document_id}" + self.assertTrue(actual_call_url.endswith(expected_url)) + + called_params = mock_get.call_args[1]['params'] + self.assertEqual(called_params, None) + + @patch("requests.get") + def test_get_daily_activity_document_error(self, mock_get): + mock_get.side_effect = RequestException("API error") + document_id = "test_document_id" + with self.assertRaises(RequestException): + self.client.daily_activity.get_daily_activity_document(document_id=document_id) + +class TestDailySleep(unittest.TestCase): + def setUp(self): + + self.client = OuraClient(access_token="test_token") + + @patch("requests.get") + def test_get_daily_sleep_documents(self, mock_get): + mock_data = [ + { + "id": "sleep_id_1", + "contributors": { + "deep_sleep": 70, + "efficiency": 80, + "latency": 90, + "rem_sleep": 60, + "restfulness": 75, + "timing": 85, + "total_sleep": 95, + }, + "day": "2024-03-10", + "timestamp": "2024-03-10T22:00:00+00:00", + }, + { + "id": "sleep_id_2", + "contributors": { + "deep_sleep": 75, + "efficiency": 85, + "latency": 95, + "rem_sleep": 65, + "restfulness": 80, + "timing": 90, + "total_sleep": 100, + }, + "day": "2024-03-11", + "timestamp": "2024-03-11T22:00:00+00:00", + }, + ] + mock_response_json = {"data": mock_data, "next_token": "next_sleep_token"} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + start_date_str = "2024-03-10" + end_date_str = "2024-03-11" + start_date = date.fromisoformat(start_date_str) + end_date = date.fromisoformat(end_date_str) + + daily_sleep_response = ( + self.client.daily_sleep.get_daily_sleep_documents( + start_date=start_date, + end_date=end_date, + next_token="test_sleep_token" + ) + ) + + self.assertIsInstance(daily_sleep_response, DailySleepResponse) + self.assertEqual(len(daily_sleep_response.data), 2) + self.assertIsInstance(daily_sleep_response.data[0], DailySleepModel) + self.assertIsInstance( + daily_sleep_response.data[0].contributors, + SleepContributors + ) + self.assertEqual(daily_sleep_response.next_token, "next_sleep_token") + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/daily_sleep", + headers=self.client.headers, + params={ + "start_date": start_date_str, + "end_date": end_date_str, + "next_token": "test_sleep_token", + }, + + ) + + @patch("requests.get") + def test_get_daily_sleep_documents_with_string_dates(self, mock_get): + mock_data = [ + { + "id": "sleep_id_1", + "contributors": {"deep_sleep": 70}, + "day": "2024-03-10", + "timestamp": "2024-03-10T22:00:00+00:00", + } + ] + mock_response_json = {"data": mock_data, "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + start_date_str = "2024-03-10" + end_date_str = "2024-03-11" + + self.client.daily_sleep.get_daily_sleep_documents( + start_date=start_date_str, end_date=end_date_str + ) + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/daily_sleep", + headers=self.client.headers, + params={"start_date": start_date_str, "end_date": end_date_str}, + + ) + + @patch("requests.get") + def test_get_daily_sleep_documents_error(self, mock_get): + mock_get.side_effect = RequestException("API error") + with self.assertRaises(RequestException): + self.client.daily_sleep.get_daily_sleep_documents( + start_date="2024-03-10", end_date="2024-03-11" + ) + + @patch("requests.get") + def test_get_daily_sleep_document(self, mock_get): + mock_response_json = { + "id": "test_sleep_document_id", + "contributors": { + "deep_sleep": 70, + "efficiency": 80, + "latency": 90, + "rem_sleep": 60, + "restfulness": 75, + "timing": 85, + "total_sleep": 95, + }, + "day": "2024-03-10", + "timestamp": "2024-03-10T22:00:00+00:00", + "score": 85, + "bedtime_end": "2024-03-11T07:00:00+00:00", + "bedtime_start": "2024-03-10T22:00:00+00:00", + "type": "main_sleep", + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + document_id = "test_sleep_document_id" + daily_sleep_document = self.client.daily_sleep.get_daily_sleep_document( + document_id=document_id + ) + + self.assertIsInstance(daily_sleep_document, DailySleepModel) + self.assertEqual(daily_sleep_document.id, document_id) + self.assertIsInstance( + daily_sleep_document.contributors, SleepContributors + ) + self.assertEqual(daily_sleep_document.score, 85) +<<<<<<< HEAD + self.assertEqual(daily_sleep_document.bedtime_end, datetime.fromisoformat("2024-03-11T07:00:00+00:00")) + self.assertEqual(daily_sleep_document.bedtime_start, datetime.fromisoformat("2024-03-10T22:00:00+00:00")) +======= + self.assertEqual( + daily_sleep_document.bedtime_end, + datetime.fromisoformat("2024-03-11T07:00:00+00:00") + ) + self.assertEqual( + daily_sleep_document.bedtime_start, + datetime.fromisoformat("2024-03-10T22:00:00+00:00") + ) +>>>>>>> cd7b1320f6e9ecc96b943f9eaa71c4a664f66e3f + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/daily_sleep/{document_id}", + headers=self.client.headers, + params=None, +<<<<<<< HEAD +======= + +>>>>>>> cd7b1320f6e9ecc96b943f9eaa71c4a664f66e3f + ) + + @patch("requests.get") + def test_get_daily_sleep_document_error(self, mock_get): + mock_get.side_effect = RequestException("API error") + document_id = "test_sleep_document_id" + with self.assertRaises(RequestException): + self.client.daily_sleep.get_daily_sleep_document(document_id=document_id) + +class TestDailyReadiness(unittest.TestCase): + def setUp(self): + + self.client = OuraClient(access_token="test_token") + + @patch("requests.get") + def test_get_daily_readiness_documents(self, mock_get): + mock_data = [ + { + "id": "readiness_id_1", + "contributors": {"activity_balance": 60, "body_temperature": 70}, + "day": "2024-03-10", + "score": 75, + "timestamp": "2024-03-10T00:00:00+00:00", + }, + { + "id": "readiness_id_2", + "contributors": {"activity_balance": 65, "body_temperature": 75}, + "day": "2024-03-11", + "score": 80, + "timestamp": "2024-03-11T00:00:00+00:00", + }, + ] + mock_response_json = {"data": mock_data, "next_token": "next_readiness_token"} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + start_date_str = "2024-03-10" + end_date_str = "2024-03-11" + start_date = date.fromisoformat(start_date_str) + end_date = date.fromisoformat(end_date_str) + + daily_readiness_response = ( + self.client.daily_readiness.get_daily_readiness_documents( + start_date=start_date, + end_date=end_date, + next_token="test_readiness_token" + ) + ) + + self.assertIsInstance(daily_readiness_response, DailyReadinessResponse) + self.assertEqual(len(daily_readiness_response.data), 2) + self.assertIsInstance( + daily_readiness_response.data[0], DailyReadinessModel + ) + self.assertIsInstance( + daily_readiness_response.data[0].contributors, + ReadinessContributors + ) + self.assertEqual( + daily_readiness_response.next_token, "next_readiness_token" + ) + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/daily_readiness", + headers=self.client.headers, + params={ + "start_date": start_date_str, + "end_date": end_date_str, + "next_token": "test_readiness_token", + }, + + ) + + @patch("requests.get") + def test_get_daily_readiness_documents_with_string_dates(self, mock_get): + mock_data = [ + { + "id": "readiness_id_1", + "contributors": {"activity_balance": 60}, + "day": "2024-03-10", + "score": 75, + "timestamp": "2024-03-10T00:00:00+00:00", + } + ] + mock_response_json = {"data": mock_data, "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + start_date_str = "2024-03-10" + end_date_str = "2024-03-11" + + self.client.daily_readiness.get_daily_readiness_documents( + start_date=start_date_str, end_date=end_date_str + ) + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/daily_readiness", + headers=self.client.headers, + params={"start_date": start_date_str, "end_date": end_date_str}, + + ) + + @patch("requests.get") + def test_get_daily_readiness_documents_error(self, mock_get): + mock_get.side_effect = RequestException("API error") + with self.assertRaises(RequestException): + self.client.daily_readiness.get_daily_readiness_documents( + start_date="2024-03-10", end_date="2024-03-11" + ) + + @patch("requests.get") + def test_get_daily_readiness_document(self, mock_get): + mock_response_json = { + "id": "test_readiness_document_id", + "contributors": { + "activity_balance": 60, + "body_temperature": 70, + "hrv_balance": 80, + "previous_day_activity": 90, + "previous_night": 50, + "recovery_index": 65, + "resting_heart_rate": 75, + "sleep_balance": 85, + }, + "day": "2024-03-10", + "score": 78, + "temperature_trend_deviation": 0.1, + "timestamp": "2024-03-10T00:00:00+00:00", + "activity_class_5_min": "some_activity_class", # New field + "hrv_balance_data": "some_hrv_data", # New field + "spo2_percentage": 98.5, # New field + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + document_id = "test_readiness_document_id" + daily_readiness_document = ( + self.client.daily_readiness.get_daily_readiness_document( + document_id=document_id + ) + ) + + self.assertIsInstance(daily_readiness_document, DailyReadinessModel) + self.assertEqual(daily_readiness_document.id, document_id) + self.assertIsInstance( + daily_readiness_document.contributors, + ReadinessContributors + ) + self.assertEqual(daily_readiness_document.score, 78) + self.assertEqual( + daily_readiness_document.activity_class_5_min, + "some_activity_class" + ) + self.assertEqual(daily_readiness_document.hrv_balance_data, "some_hrv_data") + self.assertEqual(daily_readiness_document.spo2_percentage, 98.5) + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/daily_readiness/{document_id}", + headers=self.client.headers, + params=None, +<<<<<<< HEAD +======= + +>>>>>>> cd7b1320f6e9ecc96b943f9eaa71c4a664f66e3f + ) + + @patch("requests.get") + def test_get_daily_readiness_document_error(self, mock_get): + mock_get.side_effect = RequestException("API error") + document_id = "test_readiness_document_id" + with self.assertRaises(RequestException): + self.client.daily_readiness.get_daily_readiness_document( + document_id=document_id + ) + +class TestSleep(unittest.TestCase): + def setUp(self): + + self.client = OuraClient(access_token="test_token") + + @patch("requests.get") + def test_get_sleep_documents(self, mock_get): + # Reused from DailySleep for consistency + mock_contributors_data = { +<<<<<<< HEAD + "deep_sleep": 70, "efficiency": 80, "latency": 90, "rem_sleep": 60, + "restfulness": 75, "timing": 85, "total_sleep": 95, + } + mock_readiness_contributors_data = { # Reused from DailyReadiness + "activity_balance": 60, "body_temperature": 70, "hrv_balance": 80, + "previous_day_activity": 90, "previous_night": 50, "recovery_index": 65, +======= + "deep_sleep": 70, "efficiency": 80, + "latency": 90, "rem_sleep": 60, + "restfulness": 75, "timing": 85, "total_sleep": 95, + } + # Reused from DailyReadiness + mock_readiness_contributors_data = { + "activity_balance": 60, "body_temperature": 70, + "hrv_balance": 80, + "previous_day_activity": 90, "previous_night": 50, + "recovery_index": 65, +>>>>>>> cd7b1320f6e9ecc96b943f9eaa71c4a664f66e3f + "resting_heart_rate": 75, "sleep_balance": 85, + } + mock_data = [ + { + "id": "sleep_doc_1", + "average_breath": 14.5, + "average_heart_rate": 58.0, + "average_hrv": 65, + "awake_time": 3600, + "bedtime_end": "2024-03-11T07:00:00+00:00", + "bedtime_start": "2024-03-10T22:00:00+00:00", + "day": "2024-03-10", + "deep_sleep_duration": 7200, + "efficiency": 90, + "latency": 600, + "light_sleep_duration": 18000, + "period": 1, + "readiness": mock_readiness_contributors_data, # Nested model + "rem_sleep_duration": 3600, + "score": 85, + "contributors": mock_contributors_data, # Nested SleepContributors + "type": "main_sleep", + "timestamp": "2024-03-10T22:00:00+00:00", # Added timestamp for SleepModel + }, + ] + mock_response_json = { + "data": mock_data, + "next_token": "next_sleep_doc_token" + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + start_date_str = "2024-03-10" + end_date_str = "2024-03-11" + start_date = date.fromisoformat(start_date_str) + end_date = date.fromisoformat(end_date_str) + + sleep_response = self.client.sleep.get_sleep_documents( + start_date=start_date, + end_date=end_date, + next_token="test_sleep_doc_token" + ) + + self.assertIsInstance(sleep_response, SleepResponse) + self.assertEqual(len(sleep_response.data), 1) + self.assertIsInstance(sleep_response.data[0], SleepModel) + self.assertIsInstance( + + sleep_response.data[0].contributors, + SleepContributors + + ) + self.assertIsInstance( + sleep_response.data[0].readiness, ReadinessContributors + ) + self.assertEqual(sleep_response.next_token, "next_sleep_doc_token") + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/sleep", + headers=self.client.headers, + params={ + "start_date": start_date_str, + "end_date": end_date_str, + "next_token": "test_sleep_doc_token", + }, + + ) + + @patch("requests.get") + def test_get_sleep_documents_with_string_dates(self, mock_get): + # Simplified mock data for this test +<<<<<<< HEAD + mock_data = [{"id": "sleep_doc_str_date", "day": "2024-03-10", "contributors": {"deep_sleep": 1}, "timestamp": "2024-03-10T22:00:00+00:00"}] +======= + mock_data = [{ + "id": "sleep_doc_str_date", + "day": "2024-03-10", + "contributors": {"deep_sleep": 1}, + "timestamp": "2024-03-10T22:00:00+00:00" + }] +>>>>>>> cd7b1320f6e9ecc96b943f9eaa71c4a664f66e3f + mock_response_json = {"data": mock_data, "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + start_date_str = "2024-03-10" + end_date_str = "2024-03-11" + + self.client.sleep.get_sleep_documents( + start_date=start_date_str, end_date=end_date_str + ) + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/sleep", + headers=self.client.headers, + params={"start_date": start_date_str, "end_date": end_date_str}, + + ) + + @patch("requests.get") + def test_get_sleep_documents_error(self, mock_get): + mock_get.side_effect = RequestException("API error") + with self.assertRaises(RequestException): + self.client.sleep.get_sleep_documents( + start_date="2024-03-10", end_date="2024-03-11" + ) + + @patch("requests.get") + def test_get_sleep_document(self, mock_get): + mock_contributors_data = {"deep_sleep": 70} + mock_readiness_contributors_data = {"activity_balance": 60} + mock_response_json = { + "id": "test_sleep_doc_single", + "average_breath": 14.2, + "day": "2024-03-10", + "bedtime_end": "2024-03-11T07:30:00+00:00", + "bedtime_start": "2024-03-10T22:15:00+00:00", + "contributors": mock_contributors_data, + "readiness": mock_readiness_contributors_data, + "score": 88, + "type": "main_sleep", + "timestamp": "2024-03-10T22:15:00+00:00", + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + document_id = "test_sleep_doc_single" + sleep_document = self.client.sleep.get_sleep_document( + document_id=document_id + ) + + self.assertIsInstance(sleep_document, SleepModel) + self.assertEqual(sleep_document.id, document_id) + self.assertIsInstance(sleep_document.contributors, SleepContributors) + self.assertIsInstance(sleep_document.readiness, ReadinessContributors) + self.assertEqual(sleep_document.score, 88) + self.assertEqual( + sleep_document.bedtime_end, + datetime.fromisoformat("2024-03-11T07:30:00+00:00") + ) + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/sleep/{document_id}", + headers=self.client.headers, + params=None, + + ) + + @patch("requests.get") + def test_get_sleep_document_error(self, mock_get): + mock_get.side_effect = RequestException("API error") + document_id = "test_sleep_doc_single_error" + with self.assertRaises(RequestException): + self.client.sleep.get_sleep_document(document_id=document_id) + +class TestSession(unittest.TestCase): + def setUp(self): + + self.client = OuraClient(access_token="test_token") + + @patch("requests.get") + def test_get_session_documents(self, mock_get): + mock_data = [ + { + "id": "session_doc_1", + "day": "2024-03-10", + "start_datetime": "2024-03-10T10:00:00+00:00", + "end_datetime": "2024-03-10T10:30:00+00:00", + "type": "meditation", + "mood": "good", + "duration": 1800, + }, + { + "id": "session_doc_2", + "day": "2024-03-11", + "start_datetime": "2024-03-11T14:00:00+00:00", + "end_datetime": "2024-03-11T14:20:00+00:00", + "type": "nap", + "duration": 1200, + }, + ] + mock_response_json = {"data": mock_data, "next_token": "next_session_token"} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + start_date_str = "2024-03-10" + end_date_str = "2024-03-11" + start_date = date.fromisoformat(start_date_str) + end_date = date.fromisoformat(end_date_str) + + session_response = self.client.session.get_session_documents( + start_date=start_date, + end_date=end_date, + next_token="test_session_token" + ) + + self.assertIsInstance(session_response, SessionResponse) + self.assertEqual(len(session_response.data), 2) + self.assertIsInstance(session_response.data[0], SessionModel) + self.assertEqual(session_response.next_token, "next_session_token") + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/session", + headers=self.client.headers, + params={ + "start_date": start_date_str, + "end_date": end_date_str, + "next_token": "test_session_token", + }, + + ) + + @patch("requests.get") + def test_get_session_documents_with_string_dates(self, mock_get): + mock_data = [ + { + "id": "session_doc_str", + "day": "2024-03-10", + "start_datetime": "2024-03-10T10:00:00+00:00", + "end_datetime": "2024-03-10T10:30:00+00:00", + "type": "meditation", + } + ] + mock_response_json = {"data": mock_data, "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + start_date_str = "2024-03-10" + end_date_str = "2024-03-11" + + self.client.session.get_session_documents( + start_date=start_date_str, end_date=end_date_str + ) + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/session", + headers=self.client.headers, + params={"start_date": start_date_str, "end_date": end_date_str}, + + ) + + @patch("requests.get") + def test_get_session_documents_error(self, mock_get): + mock_get.side_effect = RequestException("API error") + with self.assertRaises(RequestException): + self.client.session.get_session_documents( + start_date="2024-03-10", end_date="2024-03-11" + ) + + @patch("requests.get") + def test_get_session_document(self, mock_get): + mock_response_json = { + "id": "test_session_single", + "day": "2024-03-10", + "start_datetime": "2024-03-10T15:00:00+00:00", + "end_datetime": "2024-03-10T15:45:00+00:00", + "type": "workout", + "mood": "great", + "duration": 2700, + "energy": 250.0, + "stress": 10.5, + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + document_id = "test_session_single" + session_document = self.client.session.get_session_document( + document_id=document_id + ) + + self.assertIsInstance(session_document, SessionModel) + self.assertEqual(session_document.id, document_id) + self.assertEqual(session_document.type, "workout") + self.assertEqual(session_document.mood, "great") + self.assertEqual(session_document.duration, 2700) + self.assertEqual( + session_document.start_datetime, + datetime.fromisoformat("2024-03-10T15:00:00+00:00") + ) + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/session/{document_id}", + headers=self.client.headers, + params=None, + + ) + + @patch("requests.get") + def test_get_session_document_error(self, mock_get): + mock_get.side_effect = RequestException("API error") + document_id = "test_session_single_error" + with self.assertRaises(RequestException): + self.client.session.get_session_document(document_id=document_id) + +class TestTag(unittest.TestCase): + def setUp(self): + + self.client = OuraClient(access_token="test_token") + + @patch("requests.get") + def test_get_tag_documents(self, mock_get): + mock_data = [ + { + "id": "tag_doc_1", + "day": "2024-03-10", + "text": "Morning workout", + "timestamp": "2024-03-10T09:00:00+00:00", + }, + { + "id": "tag_doc_2", + "day": "2024-03-11", + "text": "Big presentation", + "timestamp": "2024-03-11T14:00:00+00:00", + }, + ] + mock_response_json = {"data": mock_data, "next_token": "next_tag_token"} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + start_date_str = "2024-03-10" + end_date_str = "2024-03-11" + start_date = date.fromisoformat(start_date_str) + end_date = date.fromisoformat(end_date_str) + + tag_response = self.client.tag.get_tag_documents( + start_date=start_date, + end_date=end_date, + next_token="test_tag_token" + ) + + self.assertIsInstance(tag_response, TagResponse) + self.assertEqual(len(tag_response.data), 2) + self.assertIsInstance(tag_response.data[0], TagModel) + self.assertEqual(tag_response.next_token, "next_tag_token") + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/tag", + headers=self.client.headers, + params={ + "start_date": start_date_str, + "end_date": end_date_str, + "next_token": "test_tag_token", + }, + + ) + + @patch("requests.get") + def test_get_tag_documents_with_string_dates(self, mock_get): + mock_data = [ + { + "id": "tag_doc_str", + "day": "2024-03-10", + "text": "String date test", + "timestamp": "2024-03-10T09:00:00+00:00", + } + ] + mock_response_json = {"data": mock_data, "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + start_date_str = "2024-03-10" + end_date_str = "2024-03-11" + + self.client.tag.get_tag_documents( + start_date=start_date_str, end_date=end_date_str + ) + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/tag", + headers=self.client.headers, + params={"start_date": start_date_str, "end_date": end_date_str}, + + ) + + @patch("requests.get") + def test_get_tag_documents_error(self, mock_get): + mock_get.side_effect = RequestException("API error") + with self.assertRaises(RequestException): + self.client.tag.get_tag_documents( + start_date="2024-03-10", end_date="2024-03-11" + ) + + @patch("requests.get") + def test_get_tag_document(self, mock_get): + mock_response_json = { + "id": "test_tag_single", + "day": "2024-03-10", + "text": "Single tag test", + "timestamp": "2024-03-10T11:00:00+00:00", + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + document_id = "test_tag_single" + tag_document = self.client.tag.get_tag_document( + document_id=document_id + ) + + self.assertIsInstance(tag_document, TagModel) + self.assertEqual(tag_document.id, document_id) + self.assertEqual(tag_document.text, "Single tag test") + self.assertEqual( + tag_document.timestamp, + datetime.fromisoformat("2024-03-10T11:00:00+00:00") + ) + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/tag/{document_id}", + headers=self.client.headers, + params=None, + + ) + + @patch("requests.get") + def test_get_tag_document_error(self, mock_get): + mock_get.side_effect = RequestException("API error") + document_id = "test_tag_single_error" + with self.assertRaises(RequestException): + self.client.tag.get_tag_document(document_id=document_id) + +class TestWorkout(unittest.TestCase): + def setUp(self): + + self.client = OuraClient(access_token="test_token") + + @patch("requests.get") + def test_get_workout_documents(self, mock_get): + mock_data = [ + { + "id": "workout_doc_1", + "activity": "running", + "calories": 300.5, + "day": "2024-03-10", + "distance": 5000.0, + "end_datetime": "2024-03-10T08:30:00+00:00", + "intensity": "moderate", + "source": "manual", + "start_datetime": "2024-03-10T08:00:00+00:00", + }, + { + "id": "workout_doc_2", + "activity": "yoga", + "day": "2024-03-11", + "end_datetime": "2024-03-11T17:00:00+00:00", + "intensity": "easy", + "source": "oura_app", + "start_datetime": "2024-03-11T16:00:00+00:00", + }, + ] + mock_response_json = {"data": mock_data, "next_token": "next_workout_token"} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + start_date_str = "2024-03-10" + end_date_str = "2024-03-11" + start_date = date.fromisoformat(start_date_str) + end_date = date.fromisoformat(end_date_str) + + workout_response = self.client.workout.get_workout_documents( + start_date=start_date, + end_date=end_date, + next_token="test_workout_token" + ) + + self.assertIsInstance(workout_response, WorkoutResponse) + self.assertEqual(len(workout_response.data), 2) + self.assertIsInstance(workout_response.data[0], WorkoutModel) + self.assertEqual(workout_response.next_token, "next_workout_token") + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/workout", + headers=self.client.headers, + params={ + "start_date": start_date_str, + "end_date": end_date_str, + "next_token": "test_workout_token", + }, + + ) + + @patch("requests.get") + def test_get_workout_documents_with_string_dates(self, mock_get): + mock_data = [ + { + "id": "workout_doc_str", + "activity": "cycling", + "day": "2024-03-10", + "end_datetime": "2024-03-10T18:00:00+00:00", + "intensity": "hard", + "source": "strava", + "start_datetime": "2024-03-10T17:00:00+00:00", + } + ] + mock_response_json = {"data": mock_data, "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + start_date_str = "2024-03-10" + end_date_str = "2024-03-11" + + self.client.workout.get_workout_documents( + start_date=start_date_str, end_date=end_date_str + ) + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/workout", + headers=self.client.headers, + params={"start_date": start_date_str, "end_date": end_date_str}, + + ) + + @patch("requests.get") + def test_get_workout_documents_error(self, mock_get): + mock_get.side_effect = RequestException("API error") + with self.assertRaises(RequestException): + self.client.workout.get_workout_documents( + start_date="2024-03-10", end_date="2024-03-11" + ) + + @patch("requests.get") + def test_get_workout_document(self, mock_get): + mock_response_json = { + "id": "test_workout_single", + "activity": "swimming", + "calories": 400.0, + "day": "2024-03-10", + "distance": 1000.0, + "end_datetime": "2024-03-10T12:45:00+00:00", + "energy": 1673.6, # Example energy in kJ + "intensity": "moderate", + "label": "Pool session", + "source": "apple_health", + "start_datetime": "2024-03-10T12:00:00+00:00", + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + document_id = "test_workout_single" + workout_document = self.client.workout.get_workout_document( + document_id=document_id + ) + + self.assertIsInstance(workout_document, WorkoutModel) + self.assertEqual(workout_document.id, document_id) + self.assertEqual(workout_document.activity, "swimming") + self.assertEqual(workout_document.intensity, "moderate") + self.assertEqual(workout_document.source, "apple_health") + self.assertEqual( + workout_document.start_datetime, + datetime.fromisoformat("2024-03-10T12:00:00+00:00") + ) + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/workout/{document_id}", + headers=self.client.headers, + params=None, + + ) + + @patch("requests.get") + def test_get_workout_document_error(self, mock_get): + mock_get.side_effect = RequestException("API error") + document_id = "test_workout_single_error" + with self.assertRaises(RequestException): + self.client.workout.get_workout_document(document_id=document_id) + +class TestEnhancedTag(unittest.TestCase): + def setUp(self): + + self.client = OuraClient(access_token="test_token") + + @patch("requests.get") + def test_get_enhanced_tag_documents(self, mock_get): + mock_data = [ + { + "id": "tag_1", + "tag_type_code": "common_cold", + "start_time": "2024-03-10T00:00:00+00:00", + "end_time": "2024-03-12T00:00:00+00:00", + "start_day": "2024-03-10", + "end_day": "2024-03-12", + "comment": "Feeling under the weather." + }, + { + "id": "tag_2", + "tag_type_code": "vacation", + "start_time": "2024-03-15T00:00:00+00:00", + "start_day": "2024-03-15", + "comment": "Beach time!" + }, + ] + mock_response_json = { + "data": mock_data, + "next_token": "next_enhanced_tag_token" + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + start_date_str = "2024-03-01" # Using different dates for query + end_date_str = "2024-03-31" + start_date = date.fromisoformat(start_date_str) + end_date = date.fromisoformat(end_date_str) + + enhanced_tag_response = self.client.enhanced_tag.get_enhanced_tag_documents( + start_date=start_date, + end_date=end_date, + next_token="test_enhanced_tag_token" + ) + + self.assertIsInstance(enhanced_tag_response, EnhancedTagResponse) + self.assertEqual(len(enhanced_tag_response.data), 2) + self.assertIsInstance( + enhanced_tag_response.data[0], EnhancedTagModel + ) + self.assertEqual( + enhanced_tag_response.next_token, "next_enhanced_tag_token" + ) + self.assertEqual(enhanced_tag_response.data[0].tag_type_code, "common_cold") +<<<<<<< HEAD + self.assertEqual(enhanced_tag_response.data[1].start_day, date(2024, 3, 15)) +======= + self.assertEqual( + enhanced_tag_response.data[1].start_day, date(2024, 3, 15) + ) +>>>>>>> cd7b1320f6e9ecc96b943f9eaa71c4a664f66e3f + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/enhanced_tag", + headers=self.client.headers, + params={ + "start_date": start_date_str, + "end_date": end_date_str, + "next_token": "test_enhanced_tag_token", + }, + + ) + + @patch("requests.get") + def test_get_enhanced_tag_documents_with_string_dates(self, mock_get): + mock_data = [ + { + "id": "tag_str_date", + "tag_type_code": "travel", + "start_time": "2024-03-05T00:00:00+00:00", + "start_day": "2024-03-05", + } + ] + mock_response_json = {"data": mock_data, "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + start_date_str = "2024-03-01" + end_date_str = "2024-03-31" + + self.client.enhanced_tag.get_enhanced_tag_documents( + start_date=start_date_str, end_date=end_date_str + ) + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/enhanced_tag", + headers=self.client.headers, + params={"start_date": start_date_str, "end_date": end_date_str}, + + ) + + @patch("requests.get") + def test_get_enhanced_tag_documents_error(self, mock_get): + mock_get.side_effect = RequestException("API error") + with self.assertRaises(RequestException): + self.client.enhanced_tag.get_enhanced_tag_documents( + start_date="2024-03-01", end_date="2024-03-31" + ) + + @patch("requests.get") + def test_get_enhanced_tag_document(self, mock_get): + mock_response_json = { + "id": "test_enhanced_tag_single", + "tag_type_code": "stress", + "start_time": "2024-03-10T10:00:00+00:00", + "end_time": "2024-03-10T18:00:00+00:00", + "start_day": "2024-03-10", + "end_day": "2024-03-10", + "comment": "Tough day at work." + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + document_id = "test_enhanced_tag_single" + enhanced_tag_document = self.client.enhanced_tag.get_enhanced_tag_document( + document_id=document_id + ) + + self.assertIsInstance(enhanced_tag_document, EnhancedTagModel) + self.assertEqual(enhanced_tag_document.id, document_id) + self.assertEqual(enhanced_tag_document.tag_type_code, "stress") + self.assertEqual(enhanced_tag_document.comment, "Tough day at work.") +<<<<<<< HEAD + self.assertEqual(enhanced_tag_document.start_time, datetime.fromisoformat("2024-03-10T10:00:00+00:00")) + self.assertEqual(enhanced_tag_document.end_day, date(2024, 3, 10)) +======= + self.assertEqual( + enhanced_tag_document.start_time, + datetime.fromisoformat("2024-03-10T10:00:00+00:00") + ) + self.assertEqual( + enhanced_tag_document.end_day, date(2024, 3, 10) + ) +>>>>>>> cd7b1320f6e9ecc96b943f9eaa71c4a664f66e3f + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/enhanced_tag/{document_id}", + headers=self.client.headers, + params=None, + + ) + + @patch("requests.get") + def test_get_enhanced_tag_document_error(self, mock_get): + mock_get.side_effect = RequestException("API error") + document_id = "test_enhanced_tag_single_error" + with self.assertRaises(RequestException): + self.client.enhanced_tag.get_enhanced_tag_document(document_id=document_id) + +class TestDailySpo2(unittest.TestCase): + def setUp(self): + + self.client = OuraClient(access_token="test_token") + + @patch("requests.get") + def test_get_daily_spo2_documents(self, mock_get): + mock_data = [ + { + "id": "spo2_1", + "day": "2024-03-10", + "spo2_percentage": 97.5, + "aggregated_values": {"average": 97.5}, + "timestamp": "2024-03-11T00:00:00+00:00" + }, + { + "id": "spo2_2", + "day": "2024-03-11", + "aggregated_values": {"average": 98.0}, + "timestamp": "2024-03-12T00:00:00+00:00" + }, + ] + mock_response_json = { + "data": mock_data, + "next_token": "next_spo2_token" + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + start_date_str = "2024-03-10" + end_date_str = "2024-03-11" + start_date = date.fromisoformat(start_date_str) + end_date = date.fromisoformat(end_date_str) + + daily_spo2_response = self.client.daily_spo2.get_daily_spo2_documents( + start_date=start_date, + end_date=end_date, + next_token="test_spo2_token" + ) + + self.assertIsInstance(daily_spo2_response, DailySpO2Response) + self.assertEqual(len(daily_spo2_response.data), 2) + self.assertIsInstance(daily_spo2_response.data[0], DailySpO2Model) +<<<<<<< HEAD + if daily_spo2_response.data[0].aggregated_values: # Check if aggregated_values exists + self.assertIsInstance(daily_spo2_response.data[0].aggregated_values, DailySpO2AggregatedValuesModel) +======= + # Check if aggregated_values exists + if daily_spo2_response.data[0].aggregated_values: + self.assertIsInstance( + daily_spo2_response.data[0].aggregated_values, + DailySpO2AggregatedValuesModel + ) +>>>>>>> cd7b1320f6e9ecc96b943f9eaa71c4a664f66e3f + self.assertEqual(daily_spo2_response.next_token, "next_spo2_token") + self.assertEqual(daily_spo2_response.data[0].spo2_percentage, 97.5) + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/daily_spo2", + headers=self.client.headers, + params={ + "start_date": start_date_str, + "end_date": end_date_str, + "next_token": "test_spo2_token", + }, + + ) + + @patch("requests.get") + def test_get_daily_spo2_documents_with_string_dates(self, mock_get): + mock_data = [ + { + "id": "spo2_str_date", + "day": "2024-03-10", + "aggregated_values": {"average": 96.0}, + "timestamp": "2024-03-11T00:00:00+00:00" + } + ] + mock_response_json = {"data": mock_data, "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + start_date_str = "2024-03-10" + end_date_str = "2024-03-11" + + self.client.daily_spo2.get_daily_spo2_documents( + start_date=start_date_str, end_date=end_date_str + ) + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/daily_spo2", + headers=self.client.headers, + params={"start_date": start_date_str, "end_date": end_date_str}, + + ) + + @patch("requests.get") + def test_get_daily_spo2_documents_error(self, mock_get): + mock_get.side_effect = RequestException("API error") + with self.assertRaises(RequestException): + self.client.daily_spo2.get_daily_spo2_documents( + start_date="2024-03-10", end_date="2024-03-11" + ) + + @patch("requests.get") + def test_get_daily_spo2_document(self, mock_get): + mock_response_json = { + "id": "test_spo2_single", + "day": "2024-03-10", + "spo2_percentage": 98.2, + "aggregated_values": {"average": 98.2}, + "timestamp": "2024-03-11T00:00:00+00:00" + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + document_id = "test_spo2_single" + daily_spo2_document = self.client.daily_spo2.get_daily_spo2_document( + document_id=document_id + ) + + self.assertIsInstance(daily_spo2_document, DailySpO2Model) + self.assertEqual(daily_spo2_document.id, document_id) + self.assertEqual(daily_spo2_document.spo2_percentage, 98.2) + if daily_spo2_document.aggregated_values: + self.assertEqual(daily_spo2_document.aggregated_values.average, 98.2) +<<<<<<< HEAD + self.assertEqual(daily_spo2_document.timestamp, datetime.fromisoformat("2024-03-11T00:00:00+00:00")) +======= + self.assertEqual( + daily_spo2_document.timestamp, + datetime.fromisoformat("2024-03-11T00:00:00+00:00") + ) +>>>>>>> cd7b1320f6e9ecc96b943f9eaa71c4a664f66e3f + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/daily_spo2/{document_id}", + headers=self.client.headers, + params=None, + + ) + + @patch("requests.get") + def test_get_daily_spo2_document_error(self, mock_get): + mock_get.side_effect = RequestException("API error") + document_id = "test_spo2_single_error" + with self.assertRaises(RequestException): + self.client.daily_spo2.get_daily_spo2_document(document_id=document_id) + +class TestSleepTime(unittest.TestCase): + def setUp(self): + + self.client = OuraClient(access_token="test_token") + + @patch("requests.get") + def test_get_sleep_time_documents(self, mock_get): + mock_data = [ + { + "id": "st_1", + "day": "2024-03-10", + "optimal_bedtime": { + "start_offset": -1800, + "end_offset": 3600, + "day_light_saving_time": 0 + }, + "recommendation": {"recommendation": "go_to_bed_earlier"}, + "status": {"status": "slightly_late"}, + "timestamp": "2024-03-10T04:00:00+00:00" + }, + { + "id": "st_2", + "day": "2024-03-11", +<<<<<<< HEAD + "optimal_bedtime": {"start_offset": -1500, "end_offset": 3900}, # Missing day_light_saving_time to test Optional + "recommendation": {"recommendation": "maintain_consistent_schedule"}, +======= + # Missing day_light_saving_time to test Optional + "optimal_bedtime": { + "start_offset": -1500, + "end_offset": 3900 + }, + "recommendation": { + "recommendation": "maintain_consistent_schedule" + }, +>>>>>>> cd7b1320f6e9ecc96b943f9eaa71c4a664f66e3f + "status": {"status": "optimal"}, + "timestamp": "2024-03-11T04:00:00+00:00" + }, + ] + mock_response_json = { + "data": mock_data, + "next_token": "next_sleep_time_token" + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + start_date_str = "2024-03-10" + end_date_str = "2024-03-11" + start_date = date.fromisoformat(start_date_str) + end_date = date.fromisoformat(end_date_str) + + sleep_time_response = self.client.sleep_time.get_sleep_time_documents( + start_date=start_date, + end_date=end_date, + next_token="test_sleep_time_token" + ) + + self.assertIsInstance(sleep_time_response, SleepTimeResponse) + self.assertEqual(len(sleep_time_response.data), 2) + self.assertIsInstance(sleep_time_response.data[0], SleepTimeModel) + if sleep_time_response.data[0].optimal_bedtime: +<<<<<<< HEAD + self.assertIsInstance(sleep_time_response.data[0].optimal_bedtime, SleepTimeWindow) +======= + self.assertIsInstance( + sleep_time_response.data[0].optimal_bedtime, + SleepTimeWindow + ) +>>>>>>> cd7b1320f6e9ecc96b943f9eaa71c4a664f66e3f + if sleep_time_response.data[0].recommendation: + self.assertIsInstance( + sleep_time_response.data[0].recommendation, + SleepTimeRecommendation + ) + if sleep_time_response.data[0].status: + self.assertIsInstance( + sleep_time_response.data[0].status, + SleepTimeStatus + ) + self.assertEqual(sleep_time_response.next_token, "next_sleep_time_token") + self.assertEqual(sleep_time_response.data[0].day, date(2024, 3, 10)) + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/sleep_time", + headers=self.client.headers, + params={ + "start_date": start_date_str, + "end_date": end_date_str, + "next_token": "test_sleep_time_token", + }, + + ) + + @patch("requests.get") + def test_get_sleep_time_documents_with_string_dates(self, mock_get): + mock_data = [ + { + "id": "st_str_date", + "day": "2024-03-10", + "optimal_bedtime": {"start_offset": -1800, "end_offset": 3600}, + "timestamp": "2024-03-10T04:00:00+00:00" + } + ] + mock_response_json = {"data": mock_data, "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + start_date_str = "2024-03-10" + end_date_str = "2024-03-11" + + self.client.sleep_time.get_sleep_time_documents( + start_date=start_date_str, end_date=end_date_str + ) + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/sleep_time", + headers=self.client.headers, + params={"start_date": start_date_str, "end_date": end_date_str}, + + ) + + @patch("requests.get") + def test_get_sleep_time_documents_error(self, mock_get): + mock_get.side_effect = RequestException("API error") + with self.assertRaises(RequestException): + self.client.sleep_time.get_sleep_time_documents( + start_date="2024-03-10", end_date="2024-03-11" + ) + + @patch("requests.get") + def test_get_sleep_time_document(self, mock_get): + mock_response_json = { + "id": "test_st_single", + "day": "2024-03-10", + "optimal_bedtime": { + "start_offset": -1800, + "end_offset": 3600, + "day_light_saving_time": 0 + }, + "recommendation": {"recommendation": "go_to_bed_earlier"}, + "status": {"status": "slightly_late"}, + "timestamp": "2024-03-10T04:00:00+00:00" + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + document_id = "test_st_single" + sleep_time_document = self.client.sleep_time.get_sleep_time_document( + document_id=document_id + ) + + self.assertIsInstance(sleep_time_document, SleepTimeModel) + self.assertEqual(sleep_time_document.id, document_id) + if sleep_time_document.optimal_bedtime: + self.assertEqual( + sleep_time_document.optimal_bedtime.start_offset, -1800 + ) + if sleep_time_document.recommendation: + self.assertEqual( + sleep_time_document.recommendation.recommendation, + "go_to_bed_earlier" + ) + if sleep_time_document.status: + self.assertEqual( + sleep_time_document.status.status, "slightly_late" + ) + self.assertEqual( + sleep_time_document.timestamp, + datetime.fromisoformat("2024-03-10T04:00:00+00:00") + ) + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/sleep_time/{document_id}", + headers=self.client.headers, + params=None, + + ) + + @patch("requests.get") + def test_get_sleep_time_document_error(self, mock_get): + # As per the implementation note, this endpoint might not exist. +<<<<<<< HEAD + # If it doesn't, the API would return a 404, which _make_request would raise as an HTTPError (a subclass of RequestException). +======= + # If it doesn't, the API would return a 404, which _make_request would + # raise as an HTTPError (a subclass of RequestException). +>>>>>>> cd7b1320f6e9ecc96b943f9eaa71c4a664f66e3f + mock_get.side_effect = RequestException("API error or Not Found") + document_id = "test_st_single_error" + with self.assertRaises(RequestException): + self.client.sleep_time.get_sleep_time_document(document_id=document_id) + +class TestRestModePeriod(unittest.TestCase): + def setUp(self): + + self.client = OuraClient(access_token="test_token") + + @patch("requests.get") + def test_get_rest_mode_period_documents(self, mock_get): + mock_data = [ + { + "id": "rmp_1", + "day": "2024-03-10", + "start_time": "2024-03-10T10:00:00+00:00", + "end_time": "2024-03-10T18:00:00+00:00", + "rest_mode_state": "on_demand_rest", + "baseline_heart_rate": 60, + }, + { + "id": "rmp_2", + "day": "2024-03-11", + "start_time": "2024-03-11T09:00:00+00:00", + "rest_mode_state": "recovering_from_illness", + "baseline_hrv": 50, + }, + ] + mock_response_json = {"data": mock_data, "next_token": "next_rmp_token"} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + start_date_str = "2024-03-10" + end_date_str = "2024-03-11" + start_date = date.fromisoformat(start_date_str) + end_date = date.fromisoformat(end_date_str) + + rest_mode_response = ( + self.client.rest_mode_period.get_rest_mode_period_documents( + start_date=start_date, + end_date=end_date, + next_token="test_rmp_token" + ) + ) + + self.assertIsInstance(rest_mode_response, RestModePeriodResponse) + self.assertEqual(len(rest_mode_response.data), 2) + self.assertIsInstance( + rest_mode_response.data[0], RestModePeriodModel + ) + self.assertEqual(rest_mode_response.next_token, "next_rmp_token") + self.assertEqual( + rest_mode_response.data[0].rest_mode_state, "on_demand_rest" + ) + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/rest_mode_period", + headers=self.client.headers, + params={ + "start_date": start_date_str, + "end_date": end_date_str, + "next_token": "test_rmp_token", + }, + + ) + + @patch("requests.get") + def test_get_rest_mode_period_documents_with_string_dates(self, mock_get): + mock_data = [ + { + "id": "rmp_str_date", + "day": "2024-03-10", + "start_time": "2024-03-10T10:00:00+00:00", + "rest_mode_state": "on_demand_rest", + } + ] + mock_response_json = {"data": mock_data, "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + start_date_str = "2024-03-10" + end_date_str = "2024-03-11" + + self.client.rest_mode_period.get_rest_mode_period_documents( + start_date=start_date_str, end_date=end_date_str + ) + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/rest_mode_period", + headers=self.client.headers, + params={"start_date": start_date_str, "end_date": end_date_str}, + + ) + + @patch("requests.get") + def test_get_rest_mode_period_documents_error(self, mock_get): + mock_get.side_effect = RequestException("API error") + with self.assertRaises(RequestException): + self.client.rest_mode_period.get_rest_mode_period_documents( + start_date="2024-03-10", end_date="2024-03-11" + ) + + @patch("requests.get") + def test_get_rest_mode_period_document(self, mock_get): + mock_response_json = { + "id": "test_rmp_single", + "day": "2024-03-10", + "start_time": "2024-03-10T10:00:00+00:00", + "end_time": "2024-03-10T18:00:00+00:00", + "rest_mode_state": "recovering_from_illness", + "baseline_heart_rate": 62, + "baseline_hrv": 48, + "baseline_skin_temperature": -0.2, + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + document_id = "test_rmp_single" + rmp_document = self.client.rest_mode_period.get_rest_mode_period_document( + document_id=document_id + ) + + self.assertIsInstance(rmp_document, RestModePeriodModel) + self.assertEqual(rmp_document.id, document_id) + self.assertEqual( + rmp_document.rest_mode_state, "recovering_from_illness" + ) + self.assertEqual(rmp_document.baseline_hrv, 48) + self.assertEqual( + rmp_document.start_time, + datetime.fromisoformat("2024-03-10T10:00:00+00:00") + ) + + mock_get.assert_called_once_with( + f"{self.client.BASE_URL}/v2/usercollection/rest_mode_period/{document_id}", + headers=self.client.headers, + params=None, + + ) + + @patch("requests.get") + def test_get_rest_mode_period_document_error(self, mock_get): + mock_get.side_effect = RequestException("API error") + document_id = "test_rmp_single_error" + with self.assertRaises(RequestException): + self.client.rest_mode_period.get_rest_mode_period_document( + document_id=document_id + ) + +class TestDailyStress(unittest.TestCase): + def setUp(self): + + self.client = OuraClient(access_token="test_token") + self.base_url = self.client.BASE_URL + + @patch("requests.get") + def test_get_daily_stress_documents_no_params(self, mock_get): + mock_response_data = {"data": [], "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + response = self.client.daily_stress.get_daily_stress_documents() + self.assertIsInstance(response, DailyStressResponse) + mock_get.assert_called_once_with( + f"{self.base_url}/v2/usercollection/daily_stress", + headers=self.client.headers, + params={}, + ) + + @patch("requests.get") + def test_get_daily_stress_documents_start_date(self, mock_get): + mock_response_data = {"data": [], "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + self.client.daily_stress.get_daily_stress_documents( + start_date="2024-01-01" + ) + mock_get.assert_called_once_with( + f"{self.base_url}/v2/usercollection/daily_stress", + headers=self.client.headers, + params={"start_date": "2024-01-01"}, + ) + + @patch("requests.get") + def test_get_daily_stress_documents_end_date(self, mock_get): + mock_response_data = {"data": [], "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + self.client.daily_stress.get_daily_stress_documents(end_date="2024-01-31") + mock_get.assert_called_once_with( + f"{self.base_url}/v2/usercollection/daily_stress", + headers=self.client.headers, + params={"end_date": "2024-01-31"}, + ) + + @patch("requests.get") + def test_get_daily_stress_documents_start_and_end_date(self, mock_get): + mock_response_data = {"data": [], "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + self.client.daily_stress.get_daily_stress_documents( + start_date="2024-01-01", end_date="2024-01-31" + ) + mock_get.assert_called_once_with( + f"{self.base_url}/v2/usercollection/daily_stress", + headers=self.client.headers, + params={"start_date": "2024-01-01", "end_date": "2024-01-31"}, + ) + + @patch("requests.get") + def test_get_daily_stress_documents_next_token(self, mock_get): + mock_response_data = {"data": [], "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + self.client.daily_stress.get_daily_stress_documents( + next_token="some_token" + ) + mock_get.assert_called_once_with( + f"{self.base_url}/v2/usercollection/daily_stress", + headers=self.client.headers, + params={"next_token": "some_token"}, + ) + + @patch("requests.get") + def test_get_daily_stress_documents_success(self, mock_get): + mock_data = [ + { + "id": "stress_doc_1", + "day": "2024-03-15", + "stress_high": 1200, + "recovery_high": 3600, + "day_summary": "restored", + } + ] + mock_response_json = { + "data": mock_data, + "next_token": "stress_next_token" + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + response = self.client.daily_stress.get_daily_stress_documents( + start_date="2024-03-15" + ) + self.assertIsInstance(response, DailyStressResponse) + self.assertEqual(len(response.data), 1) + self.assertIsInstance(response.data[0], DailyStressModel) + self.assertEqual(response.data[0].id, "stress_doc_1") + self.assertEqual(response.data[0].stress_high, 1200) + # Check day_summary value + self.assertEqual(response.data[0].day_summary, "restored") + self.assertEqual(response.next_token, "stress_next_token") + mock_get.assert_called_with( + f"{self.base_url}/v2/usercollection/daily_stress", + headers=self.client.headers, + params={"start_date": "2024-03-15"}, + ) + + @patch("requests.get") + def test_get_daily_stress_documents_api_error_400(self, mock_get): + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("400 Client Error") + ) + mock_get.return_value = mock_response + with self.assertRaises(requests.exceptions.HTTPError): + self.client.daily_stress.get_daily_stress_documents() + + @patch("requests.get") + def test_get_daily_stress_documents_api_error_401(self, mock_get): + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("401 Client Error") + ) + mock_get.return_value = mock_response + with self.assertRaises(requests.exceptions.HTTPError): + self.client.daily_stress.get_daily_stress_documents() + + @patch("requests.get") + def test_get_daily_stress_documents_api_error_429(self, mock_get): + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("429 Client Error") + ) + mock_get.return_value = mock_response + with self.assertRaises(requests.exceptions.HTTPError): + self.client.daily_stress.get_daily_stress_documents() + + @patch("requests.get") + def test_get_daily_stress_document_success(self, mock_get): + document_id = "sample_stress_id" + mock_response_json = { + "id": document_id, + "day": "2024-03-16", + "stress_high": 1500, + "recovery_high": 3000, + "day_summary": "stressful", + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + response = self.client.daily_stress.get_daily_stress_document(document_id) + self.assertIsInstance(response, DailyStressModel) + self.assertEqual(response.id, document_id) + self.assertEqual(response.day, date(2024, 3, 16)) + self.assertEqual(response.stress_high, 1500) + # Check day_summary value + self.assertEqual(response.day_summary, "stressful") + + mock_get.assert_called_once_with( + f"{self.base_url}/v2/usercollection/daily_stress/{document_id}", + headers=self.client.headers, + params=None, # No params for single document GET + ) + + @patch("requests.get") + def test_get_daily_stress_document_not_found_404(self, mock_get): + document_id = "non_existent_id" + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("404 Client Error: Not Found") + ) + mock_get.return_value = mock_response + + with self.assertRaises(requests.exceptions.HTTPError): + self.client.daily_stress.get_daily_stress_document(document_id) + +class TestDailyResilience(unittest.TestCase): + def setUp(self): + + self.client = OuraClient(access_token="test_token") + self.base_url = self.client.BASE_URL + + @patch("requests.get") + def test_get_daily_resilience_documents_no_params(self, mock_get): + mock_response_data = {"data": [], "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + response = self.client.daily_resilience.get_daily_resilience_documents() + self.assertIsInstance(response, DailyResilienceResponse) + mock_get.assert_called_once_with( + f"{self.base_url}/v2/usercollection/daily_resilience", + headers=self.client.headers, + params={}, + ) + + @patch("requests.get") + def test_get_daily_resilience_documents_start_date(self, mock_get): + mock_response_data = {"data": [], "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + self.client.daily_resilience.get_daily_resilience_documents( + start_date="2024-02-01" + ) + mock_get.assert_called_once_with( + f"{self.base_url}/v2/usercollection/daily_resilience", + headers=self.client.headers, + params={"start_date": "2024-02-01"}, + ) + + @patch("requests.get") + def test_get_daily_resilience_documents_end_date(self, mock_get): + mock_response_data = {"data": [], "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + self.client.daily_resilience.get_daily_resilience_documents( + end_date="2024-02-28" + ) + mock_get.assert_called_once_with( + f"{self.base_url}/v2/usercollection/daily_resilience", + headers=self.client.headers, + params={"end_date": "2024-02-28"}, + ) + + @patch("requests.get") + def test_get_daily_resilience_documents_start_and_end_date(self, mock_get): + mock_response_data = {"data": [], "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + self.client.daily_resilience.get_daily_resilience_documents( + start_date="2024-02-01", end_date="2024-02-28" + ) + mock_get.assert_called_once_with( + f"{self.base_url}/v2/usercollection/daily_resilience", + headers=self.client.headers, + params={"start_date": "2024-02-01", "end_date": "2024-02-28"}, + ) + + @patch("requests.get") + def test_get_daily_resilience_documents_next_token(self, mock_get): + mock_response_data = {"data": [], "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + self.client.daily_resilience.get_daily_resilience_documents( + next_token="res_token" + ) + mock_get.assert_called_once_with( + f"{self.base_url}/v2/usercollection/daily_resilience", + headers=self.client.headers, + params={"next_token": "res_token"}, + ) + + @patch("requests.get") + def test_get_daily_resilience_documents_success(self, mock_get): + mock_contributors_data = { + "sleep_recovery": 75.0, + "daytime_recovery": 60.0, + "stress": 80.0, + } + mock_data = [ + { + "id": "res_doc_1", + "day": "2024-03-18", + "contributors": mock_contributors_data, + "level": "solid", + } + ] + mock_response_json = { + "data": mock_data, + "next_token": "res_next_token" + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + response = self.client.daily_resilience.get_daily_resilience_documents( + start_date="2024-03-18" + ) + self.assertIsInstance(response, DailyResilienceResponse) + self.assertEqual(len(response.data), 1) + model_item = response.data[0] + self.assertIsInstance(model_item, DailyResilienceModel) + self.assertEqual(model_item.id, "res_doc_1") + # Check contributors + self.assertIsNotNone(model_item.contributors) + self.assertEqual(model_item.contributors.sleep_recovery, 75.0) + self.assertEqual(model_item.level, "solid") + self.assertEqual(response.next_token, "res_next_token") + mock_get.assert_called_with( + f"{self.base_url}/v2/usercollection/daily_resilience", + headers=self.client.headers, + params={"start_date": "2024-03-18"}, + ) + + @patch("requests.get") + def test_get_daily_resilience_documents_api_error_400(self, mock_get): + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("400 Client Error") + ) + mock_get.return_value = mock_response + with self.assertRaises(requests.exceptions.HTTPError): + self.client.daily_resilience.get_daily_resilience_documents() + + @patch("requests.get") + def test_get_daily_resilience_documents_api_error_401(self, mock_get): + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("401 Client Error") + ) + mock_get.return_value = mock_response + with self.assertRaises(requests.exceptions.HTTPError): + self.client.daily_resilience.get_daily_resilience_documents() + + @patch("requests.get") + def test_get_daily_resilience_documents_api_error_429(self, mock_get): + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("429 Client Error") + ) + mock_get.return_value = mock_response + with self.assertRaises(requests.exceptions.HTTPError): + self.client.daily_resilience.get_daily_resilience_documents() + + @patch("requests.get") + def test_get_daily_resilience_document_success(self, mock_get): + document_id = "sample_res_id" + mock_contributors_data = { + "sleep_recovery": 80.5, + "daytime_recovery": 65.2, + "stress": 70.1, + } + mock_response_json = { + "id": document_id, + "day": "2024-03-19", + "contributors": mock_contributors_data, + "level": "exceptional", + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + response = self.client.daily_resilience.get_daily_resilience_document( + document_id + ) + self.assertIsInstance(response, DailyResilienceModel) + self.assertEqual(response.id, document_id) + self.assertEqual(response.day, date(2024, 3, 19)) + # Check contributors + self.assertIsNotNone(response.contributors) + self.assertEqual(response.contributors.daytime_recovery, 65.2) + self.assertEqual(response.level, "exceptional") + + mock_get.assert_called_once_with( + f"{self.base_url}/v2/usercollection/daily_resilience/{document_id}", + headers=self.client.headers, + params=None, + ) + + @patch("requests.get") + def test_get_daily_resilience_document_not_found_404(self, mock_get): + document_id = "non_existent_res_id" + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("404 Client Error: Not Found") + ) + mock_get.return_value = mock_response + + with self.assertRaises(requests.exceptions.HTTPError): + self.client.daily_resilience.get_daily_resilience_document( + document_id + ) + +class TestDailyCardiovascularAge(unittest.TestCase): + def setUp(self): + + self.client = OuraClient(access_token="test_token") + self.base_url = self.client.BASE_URL + + @patch("requests.get") + def test_get_daily_cardiovascular_age_documents_no_params(self, mock_get): + mock_response_data = {"data": [], "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + response = ( + self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_documents() + ) + self.assertIsInstance(response, DailyCardiovascularAgeResponse) + mock_get.assert_called_once_with( + f"{self.base_url}/v2/usercollection/daily_cardiovascular_age", + headers=self.client.headers, + params={}, + ) + + @patch("requests.get") + def test_get_daily_cardiovascular_age_documents_start_date(self, mock_get): + mock_response_data = {"data": [], "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_documents( + start_date="2024-03-01" + ) + mock_get.assert_called_once_with( + f"{self.base_url}/v2/usercollection/daily_cardiovascular_age", + headers=self.client.headers, + params={"start_date": "2024-03-01"}, + ) + + @patch("requests.get") + def test_get_daily_cardiovascular_age_documents_end_date(self, mock_get): + mock_response_data = {"data": [], "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_documents( + end_date="2024-03-31" + ) + mock_get.assert_called_once_with( + f"{self.base_url}/v2/usercollection/daily_cardiovascular_age", + headers=self.client.headers, + params={"end_date": "2024-03-31"}, + ) + + @patch("requests.get") + def test_get_daily_cardiovascular_age_documents_start_and_end_date(self, mock_get): + mock_response_data = {"data": [], "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_documents( + start_date="2024-03-01", end_date="2024-03-31" + ) + mock_get.assert_called_once_with( + f"{self.base_url}/v2/usercollection/daily_cardiovascular_age", + headers=self.client.headers, + params={"start_date": "2024-03-01", "end_date": "2024-03-31"}, + ) + + @patch("requests.get") + def test_get_daily_cardiovascular_age_documents_next_token(self, mock_get): + mock_response_data = {"data": [], "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_documents( + next_token="cva_token" + ) + mock_get.assert_called_once_with( + f"{self.base_url}/v2/usercollection/daily_cardiovascular_age", + headers=self.client.headers, + params={"next_token": "cva_token"}, + ) + + @patch("requests.get") + def test_get_daily_cardiovascular_age_documents_success(self, mock_get): + mock_data = [ + { + # The API might return an 'id' but DailyCardiovascularAgeModel doesn't have it. + # This is fine, Pydantic will ignore extra fields. + "id": "cva_doc_api_id_1", + "day": "2024-03-20", + "vascular_age": 30.5, # Changed to float to match spec + } + ] + mock_response_json = { + "data": mock_data, + "next_token": "cva_next_token" + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + response = ( + self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_documents( + start_date="2024-03-20" + ) + ) + self.assertIsInstance(response, DailyCardiovascularAgeResponse) + self.assertEqual(len(response.data), 1) + model_item = response.data[0] + self.assertIsInstance(model_item, DailyCardiovascularAgeModel) + self.assertEqual(model_item.day, date(2024, 3, 20)) + self.assertEqual(model_item.vascular_age, 30.5) + self.assertEqual(response.next_token, "cva_next_token") + mock_get.assert_called_with( + f"{self.base_url}/v2/usercollection/daily_cardiovascular_age", + headers=self.client.headers, + params={"start_date": "2024-03-20"}, + ) + + @patch("requests.get") + def test_get_daily_cardiovascular_age_documents_api_error_400(self, mock_get): + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("400 Client Error") + ) + mock_get.return_value = mock_response + with self.assertRaises(requests.exceptions.HTTPError): + self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_documents() + + @patch("requests.get") + def test_get_daily_cardiovascular_age_documents_api_error_401(self, mock_get): + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("401 Client Error") + ) + mock_get.return_value = mock_response + with self.assertRaises(requests.exceptions.HTTPError): + self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_documents() + + @patch("requests.get") + def test_get_daily_cardiovascular_age_documents_api_error_429(self, mock_get): + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("429 Client Error") + ) + mock_get.return_value = mock_response + with self.assertRaises(requests.exceptions.HTTPError): + self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_documents() + + @patch("requests.get") + def test_get_daily_cardiovascular_age_document_success(self, mock_get): + document_id = "sample_cva_id" + mock_response_json = { + "id": document_id, + "day": "2024-03-21", + "vascular_age": 32.0, # Changed to float + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + response = ( + self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_document( + document_id + ) + ) + self.assertIsInstance(response, DailyCardiovascularAgeModel) + self.assertEqual(response.day, date(2024, 3, 21)) + self.assertEqual(response.vascular_age, 32.0) + + mock_get.assert_called_once_with( + f"{self.base_url}/v2/usercollection/daily_cardiovascular_age/{document_id}", + headers=self.client.headers, + params=None, + ) + + @patch("requests.get") + def test_get_daily_cardiovascular_age_document_not_found_404(self, mock_get): + document_id = "non_existent_cva_id" + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("404 Client Error: Not Found") + ) + mock_get.return_value = mock_response + + with self.assertRaises(requests.exceptions.HTTPError): + self.client.daily_cardiovascular_age.get_daily_cardiovascular_age_document( + document_id + ) + +class TestVo2Max(unittest.TestCase): + def setUp(self): + + self.client = OuraClient(access_token="test_token") + self.base_url = self.client.BASE_URL + self.correct_path_segment = "/v2/usercollection/vO2_max" # Note the casing + + @patch("requests.get") + def test_get_vo2_max_documents_no_params(self, mock_get): + mock_response_data = {"data": [], "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + response = self.client.vo2_max.get_vo2_max_documents() + self.assertIsInstance(response, Vo2MaxResponse) + mock_get.assert_called_once_with( + f"{self.base_url}{self.correct_path_segment}", + headers=self.client.headers, + params={}, + ) + + @patch("requests.get") + def test_get_vo2_max_documents_start_date(self, mock_get): + mock_response_data = {"data": [], "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + self.client.vo2_max.get_vo2_max_documents(start_date="2024-04-01") + mock_get.assert_called_once_with( + f"{self.base_url}{self.correct_path_segment}", + headers=self.client.headers, + params={"start_date": "2024-04-01"}, + ) + + @patch("requests.get") + def test_get_vo2_max_documents_end_date(self, mock_get): + mock_response_data = {"data": [], "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + self.client.vo2_max.get_vo2_max_documents(end_date="2024-04-30") + mock_get.assert_called_once_with( + f"{self.base_url}{self.correct_path_segment}", + headers=self.client.headers, + params={"end_date": "2024-04-30"}, + ) + + @patch("requests.get") + def test_get_vo2_max_documents_start_and_end_date(self, mock_get): + mock_response_data = {"data": [], "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + self.client.vo2_max.get_vo2_max_documents( + start_date="2024-04-01", end_date="2024-04-30" + ) + mock_get.assert_called_once_with( + f"{self.base_url}{self.correct_path_segment}", + headers=self.client.headers, + params={"start_date": "2024-04-01", "end_date": "2024-04-30"}, + ) + + @patch("requests.get") + def test_get_vo2_max_documents_next_token(self, mock_get): + mock_response_data = {"data": [], "next_token": None} + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_data + mock_get.return_value = mock_response + + self.client.vo2_max.get_vo2_max_documents(next_token="vo2_token") + mock_get.assert_called_once_with( + f"{self.base_url}{self.correct_path_segment}", + headers=self.client.headers, + params={"next_token": "vo2_token"}, + ) + + @patch("requests.get") + def test_get_vo2_max_documents_success(self, mock_get): + mock_data = [ + { + "id": "vo2_doc_1", + "day": "2024-04-10", + "timestamp": "2024-04-10T10:00:00+00:00", + "vo2_max": 35.5, + } + ] + mock_response_json = { + "data": mock_data, + "next_token": "vo2_next_token" + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + response = self.client.vo2_max.get_vo2_max_documents( + start_date="2024-04-10" + ) + self.assertIsInstance(response, Vo2MaxResponse) + self.assertEqual(len(response.data), 1) + model_item = response.data[0] + self.assertIsInstance(model_item, Vo2MaxModel) + self.assertEqual(model_item.id, "vo2_doc_1") + self.assertEqual(model_item.day, date(2024, 4, 10)) + self.assertEqual(model_item.vo2_max, 35.5) + self.assertEqual(response.next_token, "vo2_next_token") + mock_get.assert_called_with( + f"{self.base_url}{self.correct_path_segment}", + headers=self.client.headers, + params={"start_date": "2024-04-10"}, + ) + + @patch("requests.get") + def test_get_vo2_max_documents_api_error_400(self, mock_get): + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("400 Client Error") + ) + mock_get.return_value = mock_response + with self.assertRaises(requests.exceptions.HTTPError): + self.client.vo2_max.get_vo2_max_documents() + + @patch("requests.get") + def test_get_vo2_max_documents_api_error_401(self, mock_get): + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("401 Client Error") + ) + mock_get.return_value = mock_response + with self.assertRaises(requests.exceptions.HTTPError): + self.client.vo2_max.get_vo2_max_documents() + + @patch("requests.get") + def test_get_vo2_max_documents_api_error_429(self, mock_get): + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("429 Client Error") + ) + mock_get.return_value = mock_response + with self.assertRaises(requests.exceptions.HTTPError): + self.client.vo2_max.get_vo2_max_documents() + + @patch("requests.get") + def test_get_vo2_max_document_success(self, mock_get): + document_id = "sample_vo2_id" + mock_response_json = { + "id": document_id, + "day": "2024-04-11", + "timestamp": "2024-04-11T11:00:00+00:00", + "vo2_max": 36.2, + } + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_response_json + mock_get.return_value = mock_response + + response = self.client.vo2_max.get_vo2_max_document(document_id) + self.assertIsInstance(response, Vo2MaxModel) + self.assertEqual(response.id, document_id) + self.assertEqual(response.day, date(2024, 4, 11)) + self.assertEqual( + response.timestamp, + datetime.fromisoformat("2024-04-11T11:00:00+00:00") + ) + self.assertEqual(response.vo2_max, 36.2) + + mock_get.assert_called_once_with( + f"{self.base_url}{self.correct_path_segment}/{document_id}", + headers=self.client.headers, + params=None, + ) + + @patch("requests.get") + def test_get_vo2_max_document_not_found_404(self, mock_get): + document_id = "non_existent_vo2_id" + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = ( + requests.exceptions.HTTPError("404 Client Error: Not Found") + ) + mock_get.return_value = mock_response + + with self.assertRaises(requests.exceptions.HTTPError): + self.client.vo2_max.get_vo2_max_document(document_id)