From 2c9f479ff3e7bf3fe53c4a4dff86d749387107a2 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 22 Jun 2025 04:25:47 +0000 Subject: [PATCH 1/5] Refactor: Organize repository and enhance DevOps practices This commit reorganizes the repository structure for clarity and modularity. It also enhances DevOps practices by: - Setting up GitHub Actions for CI/CD. - Enabling Dependabot for automated dependency updates. - Integrating black for code formatting. - Dockerizing the application for easier deployment and local development. Additionally, documentation has been improved with the addition of CONTRIBUTING.md and CHANGELOG.md. --- .github/dependabot.yml | 6 + .github/workflows/python-app.yml | 9 +- =2.0.0 | 34 ++ =2.25.0 | 0 Dockerfile | 6 + .flake8 => config/.flake8 | 0 .gitignore => config/.gitignore | 0 docker-compose.yml | 8 + ARCHITECTURE.md => docs/ARCHITECTURE.md | 0 docs/CHANGELOG.md | 11 + CLAUDE.md => docs/CLAUDE.md | 0 docs/CONTRIBUTING.md | 32 ++ .../ERROR_HANDLING_SUMMARY.md | 0 PROJECT_STATE.md => docs/PROJECT_STATE.md | 0 SESSION_NOTES.md => docs/SESSION_NOTES.md | 0 .../implementation_verification_report.md | 0 requirements.txt | 4 +- example.py => src/example.py | 0 .../oura_api_client}/__init__.py | 4 +- .../oura_api_client}/api/__init__.py | 0 .../oura_api_client}/api/base.py | 0 .../oura_api_client}/api/client.py | 45 +- .../oura_api_client}/api/daily_activity.py | 9 +- .../api/daily_cardiovascular_age.py | 2 +- .../oura_api_client}/api/daily_readiness.py | 6 +- .../oura_api_client}/api/daily_resilience.py | 6 +- .../oura_api_client}/api/daily_sleep.py | 5 +- .../oura_api_client}/api/daily_spo2.py | 5 +- .../oura_api_client}/api/daily_stress.py | 5 +- .../oura_api_client}/api/enhanced_tag.py | 5 +- .../oura_api_client}/api/heartrate.py | 4 +- .../oura_api_client}/api/personal.py | 0 .../oura_api_client}/api/rest_mode_period.py | 6 +- .../api/ring_configuration.py | 20 +- .../oura_api_client}/api/session.py | 11 +- .../oura_api_client}/api/sleep.py | 12 +- .../oura_api_client}/api/sleep_time.py | 5 +- .../oura_api_client}/api/tag.py | 8 +- .../oura_api_client}/api/vo2_max.py | 8 +- .../oura_api_client}/api/webhook.py | 35 +- .../oura_api_client}/api/workout.py | 8 +- .../oura_api_client}/exceptions.py | 49 ++- .../oura_api_client}/models/__init__.py | 0 .../oura_api_client}/models/daily_activity.py | 17 +- .../models/daily_cardiovascular_age.py | 0 .../models/daily_readiness.py | 12 +- .../models/daily_resilience.py | 16 +- .../oura_api_client}/models/daily_sleep.py | 32 +- .../oura_api_client}/models/daily_spo2.py | 26 +- .../oura_api_client}/models/daily_stress.py | 4 +- .../oura_api_client}/models/enhanced_tag.py | 12 +- .../oura_api_client}/models/heartrate.py | 16 +- .../oura_api_client}/models/personal.py | 8 +- .../models/rest_mode_period.py | 12 +- .../models/ring_configuration.py | 51 +-- .../oura_api_client}/models/session.py | 50 +-- .../oura_api_client}/models/sleep.py | 10 +- .../oura_api_client}/models/sleep_time.py | 26 +- .../oura_api_client}/models/tag.py | 4 +- .../oura_api_client}/models/time_series.py | 23 +- .../oura_api_client}/models/vo2_max.py | 0 .../oura_api_client}/models/webhook.py | 43 +- .../oura_api_client}/models/workout.py | 12 +- .../oura_api_client}/utils/__init__.py | 2 +- .../oura_api_client}/utils/query_params.py | 14 +- .../oura_api_client}/utils/retry.py | 70 +-- oura_client.py => src/oura_client.py | 0 parse_openapi.py => src/parse_openapi.py | 70 ++- reference_spec.py => src/reference_spec.py | 129 ++++-- tests/test_client.py | 412 ++++++++---------- tests/test_error_handling.py | 98 ++--- wednesday_font_vector_with_holes.txt | 14 - 72 files changed, 785 insertions(+), 766 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 =2.0.0 create mode 100644 =2.25.0 create mode 100644 Dockerfile rename .flake8 => config/.flake8 (100%) rename .gitignore => config/.gitignore (100%) create mode 100644 docker-compose.yml rename ARCHITECTURE.md => docs/ARCHITECTURE.md (100%) create mode 100644 docs/CHANGELOG.md rename CLAUDE.md => docs/CLAUDE.md (100%) create mode 100644 docs/CONTRIBUTING.md rename ERROR_HANDLING_SUMMARY.md => docs/ERROR_HANDLING_SUMMARY.md (100%) rename PROJECT_STATE.md => docs/PROJECT_STATE.md (100%) rename SESSION_NOTES.md => docs/SESSION_NOTES.md (100%) rename implementation_verification_report.md => docs/implementation_verification_report.md (100%) rename example.py => src/example.py (100%) rename {oura_api_client => src/oura_api_client}/__init__.py (93%) rename {oura_api_client => src/oura_api_client}/api/__init__.py (100%) rename {oura_api_client => src/oura_api_client}/api/base.py (100%) rename {oura_api_client => src/oura_api_client}/api/client.py (89%) rename {oura_api_client => src/oura_api_client}/api/daily_activity.py (87%) rename {oura_api_client => src/oura_api_client}/api/daily_cardiovascular_age.py (98%) rename {oura_api_client => src/oura_api_client}/api/daily_readiness.py (92%) rename {oura_api_client => src/oura_api_client}/api/daily_resilience.py (92%) rename {oura_api_client => src/oura_api_client}/api/daily_sleep.py (93%) rename {oura_api_client => src/oura_api_client}/api/daily_spo2.py (94%) rename {oura_api_client => src/oura_api_client}/api/daily_stress.py (93%) rename {oura_api_client => src/oura_api_client}/api/enhanced_tag.py (93%) rename {oura_api_client => src/oura_api_client}/api/heartrate.py (91%) rename {oura_api_client => src/oura_api_client}/api/personal.py (100%) rename {oura_api_client => src/oura_api_client}/api/rest_mode_period.py (92%) rename {oura_api_client => src/oura_api_client}/api/ring_configuration.py (78%) rename {oura_api_client => src/oura_api_client}/api/session.py (81%) rename {oura_api_client => src/oura_api_client}/api/sleep.py (82%) rename {oura_api_client => src/oura_api_client}/api/sleep_time.py (95%) rename {oura_api_client => src/oura_api_client}/api/tag.py (84%) rename {oura_api_client => src/oura_api_client}/api/vo2_max.py (84%) rename {oura_api_client => src/oura_api_client}/api/webhook.py (88%) rename {oura_api_client => src/oura_api_client}/api/workout.py (85%) rename {oura_api_client => src/oura_api_client}/exceptions.py (93%) rename {oura_api_client => src/oura_api_client}/models/__init__.py (100%) rename {oura_api_client => src/oura_api_client}/models/daily_activity.py (86%) rename {oura_api_client => src/oura_api_client}/models/daily_cardiovascular_age.py (100%) rename {oura_api_client => src/oura_api_client}/models/daily_readiness.py (87%) rename {oura_api_client => src/oura_api_client}/models/daily_resilience.py (65%) rename {oura_api_client => src/oura_api_client}/models/daily_sleep.py (70%) rename {oura_api_client => src/oura_api_client}/models/daily_spo2.py (65%) rename {oura_api_client => src/oura_api_client}/models/daily_stress.py (92%) rename {oura_api_client => src/oura_api_client}/models/enhanced_tag.py (78%) rename {oura_api_client => src/oura_api_client}/models/heartrate.py (94%) rename {oura_api_client => src/oura_api_client}/models/personal.py (95%) rename {oura_api_client => src/oura_api_client}/models/rest_mode_period.py (94%) rename {oura_api_client => src/oura_api_client}/models/ring_configuration.py (62%) rename {oura_api_client => src/oura_api_client}/models/session.py (72%) rename {oura_api_client => src/oura_api_client}/models/sleep.py (94%) rename {oura_api_client => src/oura_api_client}/models/sleep_time.py (83%) rename {oura_api_client => src/oura_api_client}/models/tag.py (89%) rename {oura_api_client => src/oura_api_client}/models/time_series.py (55%) rename {oura_api_client => src/oura_api_client}/models/vo2_max.py (100%) rename {oura_api_client => src/oura_api_client}/models/webhook.py (83%) rename {oura_api_client => src/oura_api_client}/models/workout.py (83%) rename {oura_api_client => src/oura_api_client}/utils/__init__.py (92%) rename {oura_api_client => src/oura_api_client}/utils/query_params.py (96%) rename {oura_api_client => src/oura_api_client}/utils/retry.py (83%) rename oura_client.py => src/oura_client.py (100%) rename parse_openapi.py => src/parse_openapi.py (72%) rename reference_spec.py => src/reference_spec.py (86%) delete mode 100644 wednesday_font_vector_with_holes.txt diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b38df29 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 1168bd9..a4fa588 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -26,14 +26,15 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest + pip install flake8 pytest pytest-xdist if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + flake8 src tests --count --select=E9,F63,F7,F82 --show-source --statistics --config=config/.flake8 # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + flake8 src tests --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics --config=config/.flake8 - name: Test with pytest run: | - pytest + pytest -n auto -v tests diff --git a/=2.0.0 b/=2.0.0 new file mode 100644 index 0000000..b688594 --- /dev/null +++ b/=2.0.0 @@ -0,0 +1,34 @@ +Collecting requests + Using cached requests-2.32.4-py3-none-any.whl.metadata (4.9 kB) +Collecting pydantic + Downloading pydantic-2.11.7-py3-none-any.whl.metadata (67 kB) +Collecting charset_normalizer<4,>=2 (from requests) + Using cached charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (35 kB) +Collecting idna<4,>=2.5 (from requests) + Using cached idna-3.10-py3-none-any.whl.metadata (10 kB) +Collecting urllib3<3,>=1.21.1 (from requests) + Downloading urllib3-2.5.0-py3-none-any.whl.metadata (6.5 kB) +Collecting certifi>=2017.4.17 (from requests) + Using cached certifi-2025.6.15-py3-none-any.whl.metadata (2.4 kB) +Collecting annotated-types>=0.6.0 (from pydantic) + Downloading annotated_types-0.7.0-py3-none-any.whl.metadata (15 kB) +Collecting pydantic-core==2.33.2 (from pydantic) + Downloading pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (6.8 kB) +Collecting typing-extensions>=4.12.2 (from pydantic) + Using cached typing_extensions-4.14.0-py3-none-any.whl.metadata (3.0 kB) +Collecting typing-inspection>=0.4.0 (from pydantic) + Downloading typing_inspection-0.4.1-py3-none-any.whl.metadata (2.6 kB) +Using cached requests-2.32.4-py3-none-any.whl (64 kB) +Using cached charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (148 kB) +Using cached idna-3.10-py3-none-any.whl (70 kB) +Downloading urllib3-2.5.0-py3-none-any.whl (129 kB) +Downloading pydantic-2.11.7-py3-none-any.whl (444 kB) +Downloading pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.0 MB) + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.0/2.0 MB 67.1 MB/s eta 0:00:00 +Downloading annotated_types-0.7.0-py3-none-any.whl (13 kB) +Using cached certifi-2025.6.15-py3-none-any.whl (157 kB) +Using cached typing_extensions-4.14.0-py3-none-any.whl (43 kB) +Downloading typing_inspection-0.4.1-py3-none-any.whl (14 kB) +Installing collected packages: urllib3, typing-extensions, idna, charset_normalizer, certifi, annotated-types, typing-inspection, requests, pydantic-core, pydantic + +Successfully installed annotated-types-0.7.0 certifi-2025.6.15 charset_normalizer-3.4.2 idna-3.10 pydantic-2.11.7 pydantic-core-2.33.2 requests-2.32.4 typing-extensions-4.14.0 typing-inspection-0.4.1 urllib3-2.5.0 diff --git a/=2.25.0 b/=2.25.0 new file mode 100644 index 0000000..e69de29 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..de3c9ca --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.9-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install -r requirements.txt +COPY src/ ./src +CMD ["python", "src/example.py"] diff --git a/.flake8 b/config/.flake8 similarity index 100% rename from .flake8 rename to config/.flake8 diff --git a/.gitignore b/config/.gitignore similarity index 100% rename from .gitignore rename to config/.gitignore diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c72544d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +version: "3.8" +services: + app: + build: . + ports: + - "5000:5000" + volumes: + - ./src:/app/src diff --git a/ARCHITECTURE.md b/docs/ARCHITECTURE.md similarity index 100% rename from ARCHITECTURE.md rename to docs/ARCHITECTURE.md diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md new file mode 100644 index 0000000..8e660c7 --- /dev/null +++ b/docs/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- Initial project setup. diff --git a/CLAUDE.md b/docs/CLAUDE.md similarity index 100% rename from CLAUDE.md rename to docs/CLAUDE.md diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000..b143911 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# Contributing + +We welcome contributions to this project! Please follow these guidelines to ensure a smooth process. + +## Getting Started + +- Fork the repository and clone it locally. +- Create a new branch for your feature or bug fix: `git checkout -b my-feature-branch` +- Make your changes and commit them with clear, descriptive messages. +- Push your changes to your fork: `git push origin my-feature-branch` +- Open a pull request to the main repository. + +## Code Style + +- Follow PEP 8 guidelines for Python code. +- Use `black` for code formatting. +- Use `flake8` for linting. + +## Testing + +- Write unit tests for new features and bug fixes. +- Ensure all tests pass before submitting a pull request. + +## Pull Requests + +- Provide a clear description of your changes in the pull request. +- Reference any related issues in the pull request description. +- Ensure your pull request passes all CI checks. + +## Code of Conduct + +Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms. diff --git a/ERROR_HANDLING_SUMMARY.md b/docs/ERROR_HANDLING_SUMMARY.md similarity index 100% rename from ERROR_HANDLING_SUMMARY.md rename to docs/ERROR_HANDLING_SUMMARY.md diff --git a/PROJECT_STATE.md b/docs/PROJECT_STATE.md similarity index 100% rename from PROJECT_STATE.md rename to docs/PROJECT_STATE.md diff --git a/SESSION_NOTES.md b/docs/SESSION_NOTES.md similarity index 100% rename from SESSION_NOTES.md rename to docs/SESSION_NOTES.md diff --git a/implementation_verification_report.md b/docs/implementation_verification_report.md similarity index 100% rename from implementation_verification_report.md rename to docs/implementation_verification_report.md diff --git a/requirements.txt b/requirements.txt index 33263de..0ff8607 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -requests>=2.25.0 -pydantic>=2.0.0 +requests==2.32.4 +pydantic==2.11.7 diff --git a/example.py b/src/example.py similarity index 100% rename from example.py rename to src/example.py diff --git a/oura_api_client/__init__.py b/src/oura_api_client/__init__.py similarity index 93% rename from oura_api_client/__init__.py rename to src/oura_api_client/__init__.py index 058ebe1..74ddc66 100644 --- a/oura_api_client/__init__.py +++ b/src/oura_api_client/__init__.py @@ -10,7 +10,7 @@ OuraServerError, OuraClientError, OuraConnectionError, - OuraTimeoutError + OuraTimeoutError, ) from .utils import RetryConfig @@ -27,5 +27,5 @@ "OuraClientError", "OuraConnectionError", "OuraTimeoutError", - "RetryConfig" + "RetryConfig", ] diff --git a/oura_api_client/api/__init__.py b/src/oura_api_client/api/__init__.py similarity index 100% rename from oura_api_client/api/__init__.py rename to src/oura_api_client/api/__init__.py diff --git a/oura_api_client/api/base.py b/src/oura_api_client/api/base.py similarity index 100% rename from oura_api_client/api/base.py rename to src/oura_api_client/api/base.py diff --git a/oura_api_client/api/client.py b/src/oura_api_client/api/client.py similarity index 89% rename from oura_api_client/api/client.py rename to src/oura_api_client/api/client.py index 21ca4cd..a8c6c86 100644 --- a/oura_api_client/api/client.py +++ b/src/oura_api_client/api/client.py @@ -89,13 +89,13 @@ def _make_request( OuraAPIError: If the API request fails with specific error details """ # Ensure endpoint starts with / - if not endpoint.startswith('/'): + if not endpoint.startswith("/"): endpoint = f"/{endpoint}" - + # Remove any duplicate /v2 prefix if present - if endpoint.startswith('/v2/'): + if endpoint.startswith("/v2/"): endpoint = endpoint[3:] # Remove '/v2' prefix - + url = f"{self.BASE_URL}{endpoint}" # Wrap the actual request in retry logic if enabled @@ -110,26 +110,28 @@ def _make_single_request( method: str, params: Optional[Dict[str, Any]], timeout: Optional[float], - endpoint: str + endpoint: str, ) -> Dict[str, Any]: """Make a single HTTP request without retry logic. - + Args: url: Full URL to request method: HTTP method params: Query parameters timeout: Request timeout endpoint: Original endpoint for error context - + Returns: dict: The JSON response from the API - + Raises: OuraAPIError: If the request fails """ try: if method.upper() == "GET": - response = requests.get(url, headers=self.headers, params=params, timeout=timeout) + response = requests.get( + url, headers=self.headers, params=params, timeout=timeout + ) else: raise ValueError(f"HTTP method {method} is not supported yet") @@ -140,11 +142,17 @@ def _make_single_request( return response.json() except requests.exceptions.Timeout as e: - raise OuraTimeoutError(f"Request timed out after {timeout} seconds", endpoint=endpoint) from e + raise OuraTimeoutError( + f"Request timed out after {timeout} seconds", endpoint=endpoint + ) from e except requests.exceptions.ConnectionError as e: - raise OuraConnectionError(f"Failed to connect to API: {str(e)}", endpoint=endpoint) from e + raise OuraConnectionError( + f"Failed to connect to API: {str(e)}", endpoint=endpoint + ) from e except requests.exceptions.RequestException as e: - raise create_api_error(getattr(e, 'response', None), endpoint, str(e)) from e + raise create_api_error( + getattr(e, "response", None), endpoint, str(e) + ) from e def _make_request_with_retry( self, @@ -152,30 +160,31 @@ def _make_request_with_retry( method: str, params: Optional[Dict[str, Any]], timeout: Optional[float], - endpoint: str + endpoint: str, ) -> Dict[str, Any]: """Make HTTP request with retry logic. - + Args: url: Full URL to request method: HTTP method params: Query parameters timeout: Request timeout endpoint: Original endpoint for error context - + Returns: dict: The JSON response from the API - + Raises: OuraAPIError: If all retries fail """ + @retry_with_backoff( max_retries=self.retry_config.max_retries, base_delay=self.retry_config.base_delay, max_delay=self.retry_config.max_delay, - jitter=self.retry_config.jitter + jitter=self.retry_config.jitter, ) def make_request(): return self._make_single_request(url, method, params, timeout, endpoint) - + return make_request() diff --git a/oura_api_client/api/daily_activity.py b/src/oura_api_client/api/daily_activity.py similarity index 87% rename from oura_api_client/api/daily_activity.py rename to src/oura_api_client/api/daily_activity.py index c01e17b..a1f7af9 100644 --- a/oura_api_client/api/daily_activity.py +++ b/src/oura_api_client/api/daily_activity.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_activity import DailyActivityResponse, DailyActivityModel +from oura_api_client.models.daily_activity import ( + DailyActivityResponse, + DailyActivityModel, +) from oura_api_client.utils import build_query_params @@ -29,9 +32,7 @@ def get_daily_activity_documents( ) 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. diff --git a/oura_api_client/api/daily_cardiovascular_age.py b/src/oura_api_client/api/daily_cardiovascular_age.py similarity index 98% rename from oura_api_client/api/daily_cardiovascular_age.py rename to src/oura_api_client/api/daily_cardiovascular_age.py index f7f360f..c0d8ddf 100644 --- a/oura_api_client/api/daily_cardiovascular_age.py +++ b/src/oura_api_client/api/daily_cardiovascular_age.py @@ -4,7 +4,7 @@ from oura_api_client.utils import build_query_params from oura_api_client.models.daily_cardiovascular_age import ( DailyCardiovascularAgeResponse, - DailyCardiovascularAgeModel + DailyCardiovascularAgeModel, ) diff --git a/oura_api_client/api/daily_readiness.py b/src/oura_api_client/api/daily_readiness.py similarity index 92% rename from oura_api_client/api/daily_readiness.py rename to src/oura_api_client/api/daily_readiness.py index 4494ff8..4b9380b 100644 --- a/oura_api_client/api/daily_readiness.py +++ b/src/oura_api_client/api/daily_readiness.py @@ -4,7 +4,7 @@ from oura_api_client.utils import build_query_params from oura_api_client.models.daily_readiness import ( DailyReadinessResponse, - DailyReadinessModel + DailyReadinessModel, ) @@ -32,9 +32,7 @@ def get_daily_readiness_documents( ) 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. diff --git a/oura_api_client/api/daily_resilience.py b/src/oura_api_client/api/daily_resilience.py similarity index 92% rename from oura_api_client/api/daily_resilience.py rename to src/oura_api_client/api/daily_resilience.py index bdeb522..7b63007 100644 --- a/oura_api_client/api/daily_resilience.py +++ b/src/oura_api_client/api/daily_resilience.py @@ -4,7 +4,7 @@ from oura_api_client.utils import build_query_params from oura_api_client.models.daily_resilience import ( DailyResilienceResponse, - DailyResilienceModel + DailyResilienceModel, ) @@ -32,9 +32,7 @@ def get_daily_resilience_documents( ) 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. diff --git a/oura_api_client/api/daily_sleep.py b/src/oura_api_client/api/daily_sleep.py similarity index 93% rename from oura_api_client/api/daily_sleep.py rename to src/oura_api_client/api/daily_sleep.py index ce0c29c..5ef25bf 100644 --- a/oura_api_client/api/daily_sleep.py +++ b/src/oura_api_client/api/daily_sleep.py @@ -2,10 +2,7 @@ from datetime import date from oura_api_client.api.base import BaseRouter from oura_api_client.utils import build_query_params -from oura_api_client.models.daily_sleep import ( - DailySleepResponse, - DailySleepModel -) +from oura_api_client.models.daily_sleep import DailySleepResponse, DailySleepModel class DailySleep(BaseRouter): diff --git a/oura_api_client/api/daily_spo2.py b/src/oura_api_client/api/daily_spo2.py similarity index 94% rename from oura_api_client/api/daily_spo2.py rename to src/oura_api_client/api/daily_spo2.py index ea7eda1..77ec992 100644 --- a/oura_api_client/api/daily_spo2.py +++ b/src/oura_api_client/api/daily_spo2.py @@ -2,10 +2,7 @@ from datetime import date from oura_api_client.api.base import BaseRouter from oura_api_client.utils import build_query_params -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 diff --git a/oura_api_client/api/daily_stress.py b/src/oura_api_client/api/daily_stress.py similarity index 93% rename from oura_api_client/api/daily_stress.py rename to src/oura_api_client/api/daily_stress.py index e8157e2..c32739d 100644 --- a/oura_api_client/api/daily_stress.py +++ b/src/oura_api_client/api/daily_stress.py @@ -2,10 +2,7 @@ from datetime import date from oura_api_client.api.base import BaseRouter from oura_api_client.utils import build_query_params -from oura_api_client.models.daily_stress import ( - DailyStressResponse, - DailyStressModel -) +from oura_api_client.models.daily_stress import DailyStressResponse, DailyStressModel class DailyStress(BaseRouter): diff --git a/oura_api_client/api/enhanced_tag.py b/src/oura_api_client/api/enhanced_tag.py similarity index 93% rename from oura_api_client/api/enhanced_tag.py rename to src/oura_api_client/api/enhanced_tag.py index 710eb6a..4166c03 100644 --- a/oura_api_client/api/enhanced_tag.py +++ b/src/oura_api_client/api/enhanced_tag.py @@ -2,10 +2,7 @@ from datetime import date # Using date for start_date and end_date from oura_api_client.api.base import BaseRouter from oura_api_client.utils import build_query_params -from oura_api_client.models.enhanced_tag import ( - EnhancedTagResponse, - EnhancedTagModel -) +from oura_api_client.models.enhanced_tag import EnhancedTagResponse, EnhancedTagModel class EnhancedTag(BaseRouter): diff --git a/oura_api_client/api/heartrate.py b/src/oura_api_client/api/heartrate.py similarity index 91% rename from oura_api_client/api/heartrate.py rename to src/oura_api_client/api/heartrate.py index 1c74edb..41ba47c 100644 --- a/oura_api_client/api/heartrate.py +++ b/src/oura_api_client/api/heartrate.py @@ -38,9 +38,7 @@ 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/personal.py b/src/oura_api_client/api/personal.py similarity index 100% rename from oura_api_client/api/personal.py rename to src/oura_api_client/api/personal.py diff --git a/oura_api_client/api/rest_mode_period.py b/src/oura_api_client/api/rest_mode_period.py similarity index 92% rename from oura_api_client/api/rest_mode_period.py rename to src/oura_api_client/api/rest_mode_period.py index 78fd588..30c3e66 100644 --- a/oura_api_client/api/rest_mode_period.py +++ b/src/oura_api_client/api/rest_mode_period.py @@ -4,7 +4,7 @@ from oura_api_client.utils import build_query_params from oura_api_client.models.rest_mode_period import ( RestModePeriodResponse, - RestModePeriodModel + RestModePeriodModel, ) @@ -32,9 +32,7 @@ def get_rest_mode_period_documents( ) 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. diff --git a/oura_api_client/api/ring_configuration.py b/src/oura_api_client/api/ring_configuration.py similarity index 78% rename from oura_api_client/api/ring_configuration.py rename to src/oura_api_client/api/ring_configuration.py index ae767d2..8601a9b 100644 --- a/oura_api_client/api/ring_configuration.py +++ b/src/oura_api_client/api/ring_configuration.py @@ -1,10 +1,15 @@ -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 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.utils import build_query_params from oura_api_client.models.ring_configuration import ( RingConfigurationResponse, - RingConfigurationModel + RingConfigurationModel, ) @@ -14,8 +19,10 @@ def get_ring_configuration_documents( # Ring Configuration usually doesn't have start/end_date or pagination in typical REST APIs # 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 + 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 next_token: Optional[str] = None, ) -> RingConfigurationResponse: """ @@ -35,8 +42,7 @@ def get_ring_configuration_documents( params = build_query_params(start_date, end_date, next_token) response = self.client._make_request( - "/usercollection/ring_configuration", - params=params if params else None + "/usercollection/ring_configuration", params=params if params else None ) return RingConfigurationResponse(**response) diff --git a/oura_api_client/api/session.py b/src/oura_api_client/api/session.py similarity index 81% rename from oura_api_client/api/session.py rename to src/oura_api_client/api/session.py index 1083b65..f88a7aa 100644 --- a/oura_api_client/api/session.py +++ b/src/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 oura_api_client.api.base import BaseRouter from oura_api_client.models.session import SessionResponse, SessionModel @@ -11,7 +12,7 @@ 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_date: Optional[Union[str, date]] = None, # Changed from # end_datetime for consistency next_token: Optional[str] = None, ) -> SessionResponse: @@ -27,9 +28,7 @@ def get_session_documents( SessionResponse: Response containing session data. """ params = build_query_params(start_date, end_date, next_token) - response = self.client._make_request( - "/usercollection/session", params=params - ) + response = self.client._make_request("/usercollection/session", params=params) return SessionResponse(**response) def get_session_document(self, document_id: str) -> SessionModel: @@ -42,7 +41,5 @@ def get_session_document(self, document_id: str) -> SessionModel: Returns: SessionModel: Response containing session data. """ - response = self.client._make_request( - f"/usercollection/session/{document_id}" - ) + response = self.client._make_request(f"/usercollection/session/{document_id}") return SessionModel(**response) diff --git a/oura_api_client/api/sleep.py b/src/oura_api_client/api/sleep.py similarity index 82% rename from oura_api_client/api/sleep.py rename to src/oura_api_client/api/sleep.py index df29d52..c650e56 100644 --- a/oura_api_client/api/sleep.py +++ b/src/oura_api_client/api/sleep.py @@ -4,7 +4,7 @@ from oura_api_client.utils import build_query_params from oura_api_client.models.sleep import ( SleepResponse, - SleepModel # Updated model import + SleepModel, # Updated model import ) @@ -13,7 +13,7 @@ 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 + end_date: Optional[Union[str, date]] = None, # Changed # parameter name for clarity next_token: Optional[str] = None, ) -> SleepResponse: # Updated return type @@ -30,9 +30,7 @@ def get_sleep_documents( # Renamed method """ params = build_query_params(start_date, end_date, next_token) # Corrected endpoint URL from daily_sleep to sleep - response = self.client._make_request( - "/usercollection/sleep", params=params - ) + response = self.client._make_request("/usercollection/sleep", params=params) return SleepResponse(**response) def get_sleep_document( @@ -48,7 +46,5 @@ def get_sleep_document( SleepModel: Response containing sleep data. """ # Corrected endpoint URL from daily_sleep to sleep - response = self.client._make_request( - f"/usercollection/sleep/{document_id}" - ) + response = self.client._make_request(f"/usercollection/sleep/{document_id}") return SleepModel(**response) diff --git a/oura_api_client/api/sleep_time.py b/src/oura_api_client/api/sleep_time.py similarity index 95% rename from oura_api_client/api/sleep_time.py rename to src/oura_api_client/api/sleep_time.py index f8ac430..48748b3 100644 --- a/oura_api_client/api/sleep_time.py +++ b/src/oura_api_client/api/sleep_time.py @@ -2,10 +2,7 @@ from datetime import date from oura_api_client.api.base import BaseRouter from oura_api_client.utils import build_query_params -from oura_api_client.models.sleep_time import ( - SleepTimeResponse, - SleepTimeModel -) +from oura_api_client.models.sleep_time import SleepTimeResponse, SleepTimeModel class SleepTime(BaseRouter): diff --git a/oura_api_client/api/tag.py b/src/oura_api_client/api/tag.py similarity index 84% rename from oura_api_client/api/tag.py rename to src/oura_api_client/api/tag.py index f1eeb2c..57b275f 100644 --- a/oura_api_client/api/tag.py +++ b/src/oura_api_client/api/tag.py @@ -24,9 +24,7 @@ def get_tag_documents( TagResponse: Response containing tag data. """ params = build_query_params(start_date, end_date, next_token) - response = self.client._make_request( - "/usercollection/tag", params=params - ) + response = self.client._make_request("/usercollection/tag", params=params) return TagResponse(**response) def get_tag_document(self, document_id: str) -> TagModel: @@ -39,7 +37,5 @@ def get_tag_document(self, document_id: str) -> TagModel: Returns: TagModel: Response containing tag data. """ - response = self.client._make_request( - f"/usercollection/tag/{document_id}" - ) + response = self.client._make_request(f"/usercollection/tag/{document_id}") return TagModel(**response) diff --git a/oura_api_client/api/vo2_max.py b/src/oura_api_client/api/vo2_max.py similarity index 84% rename from oura_api_client/api/vo2_max.py rename to src/oura_api_client/api/vo2_max.py index e4ec3ac..8312544 100644 --- a/oura_api_client/api/vo2_max.py +++ b/src/oura_api_client/api/vo2_max.py @@ -24,9 +24,7 @@ def get_vo2_max_documents( Vo2MaxResponse: Response containing VO2 max data. """ params = build_query_params(start_date, end_date, next_token) - response = self.client._make_request( - "/usercollection/vO2_max", params=params - ) + response = self.client._make_request("/usercollection/vO2_max", params=params) return Vo2MaxResponse(**response) def get_vo2_max_document(self, document_id: str) -> Vo2MaxModel: @@ -39,7 +37,5 @@ def get_vo2_max_document(self, document_id: str) -> Vo2MaxModel: Returns: Vo2MaxModel: Response containing VO2 max data. """ - response = self.client._make_request( - f"/usercollection/vO2_max/{document_id}" - ) + response = self.client._make_request(f"/usercollection/vO2_max/{document_id}") return Vo2MaxModel(**response) diff --git a/oura_api_client/api/webhook.py b/src/oura_api_client/api/webhook.py similarity index 88% rename from oura_api_client/api/webhook.py rename to src/oura_api_client/api/webhook.py index 943a7b4..0220874 100644 --- a/oura_api_client/api/webhook.py +++ b/src/oura_api_client/api/webhook.py @@ -6,15 +6,16 @@ WebhookSubscriptionCreateRequest, WebhookSubscriptionUpdateRequest, WebhookOperation, - ExtApiV2DataType + ExtApiV2DataType, ) 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')): + 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 @@ -46,8 +47,7 @@ def list_webhook_subscriptions(self) -> List[WebhookSubscriptionModel]: """ headers = self._get_webhook_headers() response_data = self.client._make_request( - "/webhook/subscription", - headers=headers + "/webhook/subscription", headers=headers ) # API returns a list of subscriptions directly return [WebhookSubscriptionModel(**item) for item in response_data] @@ -67,8 +67,8 @@ def create_webhook_subscription( headers = self._get_webhook_headers() # Ensure Content-Type is set for POST with JSON body, if not # handled by _make_request - if 'Content-Type' not in headers: - headers['Content-Type'] = 'application/json' + if "Content-Type" not in headers: + headers["Content-Type"] = "application/json" request_body = WebhookSubscriptionCreateRequest( callback_url=callback_url, @@ -82,7 +82,7 @@ def create_webhook_subscription( json_data=request_body.model_dump( by_alias=True ), # exclude_none=True is default for model_dump - headers=headers + headers=headers, ) return WebhookSubscriptionModel(**response_data) @@ -95,8 +95,7 @@ def get_webhook_subscription( """ headers = self._get_webhook_headers() response_data = self.client._make_request( - f"/webhook/subscription/{subscription_id}", - headers=headers + f"/webhook/subscription/{subscription_id}", headers=headers ) return WebhookSubscriptionModel(**response_data) @@ -113,8 +112,8 @@ def update_webhook_subscription( API Path: PUT /v2/webhook/subscription/{subscription_id} """ headers = self._get_webhook_headers() - if 'Content-Type' not in headers: - headers['Content-Type'] = 'application/json' + if "Content-Type" not in headers: + headers["Content-Type"] = "application/json" request_body = WebhookSubscriptionUpdateRequest( verification_token=verification_token, # Now required @@ -125,10 +124,8 @@ def update_webhook_subscription( response_data = self.client._make_request( f"/webhook/subscription/{subscription_id}", method="PUT", - json_data=request_body.model_dump( - by_alias=True, exclude_none=True - ), - headers=headers + json_data=request_body.model_dump(by_alias=True, exclude_none=True), + headers=headers, ) return WebhookSubscriptionModel(**response_data) @@ -139,9 +136,7 @@ def delete_webhook_subscription(self, subscription_id: str) -> None: """ headers = self._get_webhook_headers() self.client._make_request( - f"/webhook/subscription/{subscription_id}", - method="DELETE", - headers=headers + f"/webhook/subscription/{subscription_id}", method="DELETE", headers=headers ) return None @@ -162,7 +157,7 @@ def renew_webhook_subscription( response_data = self.client._make_request( f"/webhook/subscription/renew/{subscription_id}", method="PUT", - headers=headers + 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. diff --git a/oura_api_client/api/workout.py b/src/oura_api_client/api/workout.py similarity index 85% rename from oura_api_client/api/workout.py rename to src/oura_api_client/api/workout.py index b6c559f..05a8348 100644 --- a/oura_api_client/api/workout.py +++ b/src/oura_api_client/api/workout.py @@ -24,9 +24,7 @@ def get_workout_documents( WorkoutResponse: Response containing workout data. """ params = build_query_params(start_date, end_date, next_token) - response = self.client._make_request( - "/usercollection/workout", params=params - ) + response = self.client._make_request("/usercollection/workout", params=params) return WorkoutResponse(**response) def get_workout_document(self, document_id: str) -> WorkoutModel: @@ -39,7 +37,5 @@ def get_workout_document(self, document_id: str) -> WorkoutModel: Returns: WorkoutModel: Response containing workout data. """ - response = self.client._make_request( - f"/usercollection/workout/{document_id}" - ) + response = self.client._make_request(f"/usercollection/workout/{document_id}") return WorkoutModel(**response) diff --git a/oura_api_client/exceptions.py b/src/oura_api_client/exceptions.py similarity index 93% rename from oura_api_client/exceptions.py rename to src/oura_api_client/exceptions.py index c80aa62..82c04ba 100644 --- a/oura_api_client/exceptions.py +++ b/src/oura_api_client/exceptions.py @@ -6,16 +6,16 @@ class OuraAPIError(Exception): """Base exception class for Oura API errors.""" - + def __init__( self, message: str, status_code: Optional[int] = None, response: Optional[requests.Response] = None, - endpoint: Optional[str] = None + endpoint: Optional[str] = None, ): """Initialize OuraAPIError. - + Args: message: Error message status_code: HTTP status code if available @@ -27,7 +27,7 @@ def __init__( self.status_code = status_code self.response = response self.endpoint = endpoint - + def __str__(self) -> str: """Return string representation of the error.""" parts = [self.message] @@ -40,32 +40,35 @@ def __str__(self) -> str: class OuraAuthenticationError(OuraAPIError): """Raised when authentication fails (401 Unauthorized).""" + pass class OuraAuthorizationError(OuraAPIError): """Raised when authorization fails (403 Forbidden).""" + pass class OuraNotFoundError(OuraAPIError): """Raised when a resource is not found (404 Not Found).""" + pass class OuraRateLimitError(OuraAPIError): """Raised when rate limit is exceeded (429 Too Many Requests).""" - + def __init__( self, message: str, status_code: Optional[int] = None, response: Optional[requests.Response] = None, endpoint: Optional[str] = None, - retry_after: Optional[int] = None + retry_after: Optional[int] = None, ): """Initialize OuraRateLimitError. - + Args: message: Error message status_code: HTTP status code @@ -79,38 +82,42 @@ def __init__( class OuraServerError(OuraAPIError): """Raised when server encounters an error (5xx status codes).""" + pass class OuraClientError(OuraAPIError): """Raised when client request is invalid (4xx status codes, except specific ones).""" + pass class OuraConnectionError(OuraAPIError): """Raised when connection to API fails.""" + pass class OuraTimeoutError(OuraAPIError): """Raised when request times out.""" + pass def _extract_error_message(response: requests.Response, status_code: int) -> str: """Extract error message from response. - + Args: response: HTTP response object status_code: HTTP status code - + Returns: Error message string """ try: error_data = response.json() if isinstance(error_data, dict): - message = error_data.get('error', error_data.get('message', '')) + message = error_data.get("error", error_data.get("message", "")) if message: return message except (ValueError, KeyError): @@ -120,14 +127,14 @@ def _extract_error_message(response: requests.Response, status_code: int) -> str def _extract_retry_after(response: requests.Response) -> Optional[int]: """Extract retry-after value from response headers. - + Args: response: HTTP response object - + Returns: Retry-after value in seconds, or None """ - retry_after_header = response.headers.get('Retry-After') + retry_after_header = response.headers.get("Retry-After") if retry_after_header: try: return int(retry_after_header) @@ -139,40 +146,40 @@ def _extract_retry_after(response: requests.Response) -> Optional[int]: def create_api_error( response: requests.Response, endpoint: Optional[str] = None, - message: Optional[str] = None + message: Optional[str] = None, ) -> OuraAPIError: """Create appropriate OuraAPIError based on response status code. - + Args: response: HTTP response object endpoint: The API endpoint that failed message: Custom error message (will be auto-generated if not provided) - + Returns: Appropriate OuraAPIError subclass instance """ status_code = response.status_code - + # Get error message if not message: message = _extract_error_message(response, status_code) - + # Map status codes to exception classes error_mapping = { 401: OuraAuthenticationError, 403: OuraAuthorizationError, 404: OuraNotFoundError, } - + # Check specific status codes first if status_code in error_mapping: return error_mapping[status_code](message, status_code, response, endpoint) - + # Handle rate limit error with retry-after if status_code == 429: retry_after = _extract_retry_after(response) return OuraRateLimitError(message, status_code, response, endpoint, retry_after) - + # Handle ranges if 400 <= status_code < 500: return OuraClientError(message, status_code, response, endpoint) diff --git a/oura_api_client/models/__init__.py b/src/oura_api_client/models/__init__.py similarity index 100% rename from oura_api_client/models/__init__.py rename to src/oura_api_client/models/__init__.py diff --git a/oura_api_client/models/daily_activity.py b/src/oura_api_client/models/daily_activity.py similarity index 86% rename from oura_api_client/models/daily_activity.py rename to src/oura_api_client/models/daily_activity.py index 4bd731c..b3d62c6 100644 --- a/oura_api_client/models/daily_activity.py +++ b/src/oura_api_client/models/daily_activity.py @@ -5,6 +5,7 @@ class MetData(BaseModel): """MET (Metabolic Equivalent of Task) time series data.""" + interval: float = Field(..., description="Interval between measurements in minutes") items: List[float] = Field(..., description="MET values for each interval") timestamp: str = Field(..., description="Timestamp for the data") @@ -24,12 +25,8 @@ 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" - ) + 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" ) @@ -45,16 +42,12 @@ class DailyActivityModel(BaseModel): 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_time: Optional[int] = Field(None, alias="medium_activity_time") met: Optional[MetData] = Field(None, alias="met") meters_to_target: Optional[int] = Field(None, alias="meters_to_target") non_wear_time: Optional[int] = Field(None, alias="non_wear_time") resting_time: Optional[int] = Field(None, alias="resting_time") - 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/src/oura_api_client/models/daily_cardiovascular_age.py similarity index 100% rename from oura_api_client/models/daily_cardiovascular_age.py rename to src/oura_api_client/models/daily_cardiovascular_age.py diff --git a/oura_api_client/models/daily_readiness.py b/src/oura_api_client/models/daily_readiness.py similarity index 87% rename from oura_api_client/models/daily_readiness.py rename to src/oura_api_client/models/daily_readiness.py index 4eef602..e0523ef 100644 --- a/oura_api_client/models/daily_readiness.py +++ b/src/oura_api_client/models/daily_readiness.py @@ -7,14 +7,10 @@ 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") @@ -37,9 +33,7 @@ class DailyReadinessModel(BaseModel): 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 + 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 diff --git a/oura_api_client/models/daily_resilience.py b/src/oura_api_client/models/daily_resilience.py similarity index 65% rename from oura_api_client/models/daily_resilience.py rename to src/oura_api_client/models/daily_resilience.py index 27dd8d6..46ccaf0 100644 --- a/oura_api_client/models/daily_resilience.py +++ b/src/oura_api_client/models/daily_resilience.py @@ -5,15 +5,9 @@ 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" - ) + 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): @@ -25,9 +19,7 @@ class DailyResilienceModel(BaseModel): contributors: Optional[ResilienceContributors] = Field( None, alias="contributors" ) # Resilience contributors - level: Optional[str] = Field( - None, alias="level" - ) # Resilience level + 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/src/oura_api_client/models/daily_sleep.py similarity index 70% rename from oura_api_client/models/daily_sleep.py rename to src/oura_api_client/models/daily_sleep.py index 038c10f..69e9c5b 100644 --- a/oura_api_client/models/daily_sleep.py +++ b/src/oura_api_client/models/daily_sleep.py @@ -10,7 +10,9 @@ class SleepContributors(BaseModel): rem_sleep: Optional[int] = Field(None, alias="rem_sleep") # REM sleep in minutes restfulness: Optional[int] = Field(None, alias="restfulness") timing: Optional[int] = Field(None, alias="timing") - total_sleep: Optional[int] = Field(None, alias="total_sleep") # Total sleep in minutes + total_sleep: Optional[int] = Field( + None, alias="total_sleep" + ) # Total sleep in minutes class DailySleepModel(BaseModel): @@ -33,18 +35,32 @@ class DailySleepModel(BaseModel): 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" + 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 + 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") + 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/src/oura_api_client/models/daily_spo2.py similarity index 65% rename from oura_api_client/models/daily_spo2.py rename to src/oura_api_client/models/daily_spo2.py index 213e6ea..1aae4f5 100644 --- a/oura_api_client/models/daily_spo2.py +++ b/src/oura_api_client/models/daily_spo2.py @@ -3,37 +3,25 @@ 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 +class DailySpO2AggregatedValuesModel( + BaseModel +): # Renamed from Spo2Readings to DailySpO2AggregatedValuesModel for clarity + average: Optional[float] = Field(None, alias="average") # Percentage class DailySpO2Model(BaseModel): id: str day: date spo2_percentage: Optional[float] = Field( - - - - None, alias="spo2_percentage" - - - -) # Overall percentage for the day, if available + 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 # 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/src/oura_api_client/models/daily_stress.py similarity index 92% rename from oura_api_client/models/daily_stress.py rename to src/oura_api_client/models/daily_stress.py index e1abe69..f0ed924 100644 --- a/oura_api_client/models/daily_stress.py +++ b/src/oura_api_client/models/daily_stress.py @@ -11,7 +11,7 @@ class DailyStressModel(BaseModel): ) # Duration of high stress in seconds stress_low: Optional[int] = Field( None, alias="stress_low" - ) # Duration of low stress in seconds + ) # Duration of low stress in seconds stress_medium: Optional[int] = Field( None, alias="stress_medium" ) # Duration of medium stress in seconds @@ -22,7 +22,7 @@ class DailyStressModel(BaseModel): ) # Duration of high recovery in seconds recovery_low: Optional[int] = Field( None, alias="recovery_low" - ) # Duration of low recovery in seconds + ) # Duration of low recovery in seconds recovery_medium: Optional[int] = Field( None, alias="recovery_medium" ) # Duration of medium recovery in seconds diff --git a/oura_api_client/models/enhanced_tag.py b/src/oura_api_client/models/enhanced_tag.py similarity index 78% rename from oura_api_client/models/enhanced_tag.py rename to src/oura_api_client/models/enhanced_tag.py index ce1beb9..93d3349 100644 --- a/oura_api_client/models/enhanced_tag.py +++ b/src/oura_api_client/models/enhanced_tag.py @@ -5,21 +5,13 @@ class EnhancedTagModel(BaseModel): id: str - tag_type_code: str = Field( - alias="tag_type_code" - ) # 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 + 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. diff --git a/oura_api_client/models/heartrate.py b/src/oura_api_client/models/heartrate.py similarity index 94% rename from oura_api_client/models/heartrate.py rename to src/oura_api_client/models/heartrate.py index ccd0fa2..3a96b39 100644 --- a/oura_api_client/models/heartrate.py +++ b/src/oura_api_client/models/heartrate.py @@ -7,7 +7,7 @@ class HeartRateSample(BaseModel): """Represents a single heart rate data point.""" - + timestamp: datetime bpm: int source: str @@ -15,13 +15,13 @@ class HeartRateSample(BaseModel): @classmethod def from_dict(cls, data: dict) -> "HeartRateSample": """Create a HeartRateSample from API response dictionary. - + Note: This method is kept for backward compatibility. Pydantic can parse directly from dict using HeartRateSample(**data) - + Args: data: Dictionary containing heart rate data - + Returns: HeartRateSample: Instantiated object """ @@ -30,20 +30,20 @@ def from_dict(cls, data: dict) -> "HeartRateSample": class HeartRateResponse(BaseModel): """Represents the full heart rate response.""" - + data: List[HeartRateSample] next_token: Optional[str] = None @classmethod def from_dict(cls, response: dict) -> "HeartRateResponse": """Create a HeartRateResponse from API response dictionary. - + Note: This method is kept for backward compatibility. Pydantic can parse directly from dict using HeartRateResponse(**response) - + Args: response: Dictionary containing API response - + Returns: HeartRateResponse: Instantiated object """ diff --git a/oura_api_client/models/personal.py b/src/oura_api_client/models/personal.py similarity index 95% rename from oura_api_client/models/personal.py rename to src/oura_api_client/models/personal.py index f00638e..80d97db 100644 --- a/oura_api_client/models/personal.py +++ b/src/oura_api_client/models/personal.py @@ -7,7 +7,7 @@ class PersonalInfo(BaseModel): """Represents personal information for a user.""" - + id: str email: str age: int @@ -19,13 +19,13 @@ class PersonalInfo(BaseModel): @classmethod def from_dict(cls, data: dict) -> "PersonalInfo": """Create a PersonalInfo object from API response dictionary. - + Note: This method is kept for backward compatibility. Pydantic can parse directly from dict using PersonalInfo(**data) - + Args: data: Dictionary containing personal info data - + Returns: PersonalInfo: Instantiated object """ diff --git a/oura_api_client/models/rest_mode_period.py b/src/oura_api_client/models/rest_mode_period.py similarity index 94% rename from oura_api_client/models/rest_mode_period.py rename to src/oura_api_client/models/rest_mode_period.py index 33b8ac6..d10d564 100644 --- a/oura_api_client/models/rest_mode_period.py +++ b/src/oura_api_client/models/rest_mode_period.py @@ -40,12 +40,8 @@ class RestModeEpisode(BaseModel): class RestModePeriodModel(BaseModel): id: str day: date - start_time: datetime = Field( - alias="start_time" - ) - 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" @@ -56,9 +52,7 @@ class RestModePeriodModel(BaseModel): # ) # 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" diff --git a/oura_api_client/models/ring_configuration.py b/src/oura_api_client/models/ring_configuration.py similarity index 62% rename from oura_api_client/models/ring_configuration.py rename to src/oura_api_client/models/ring_configuration.py index c888102..26bc648 100644 --- a/oura_api_client/models/ring_configuration.py +++ b/src/oura_api_client/models/ring_configuration.py @@ -1,6 +1,7 @@ from pydantic import BaseModel, Field from typing import List, Optional from datetime import datetime + # Enum-like fields will be handled with Literal from typing import Literal @@ -9,47 +10,29 @@ class RingConfigurationModel(BaseModel): id: str # Fields based on OpenAPI spec for RingConfiguration # Using Literal for fields that are described as enums - color: Optional[Literal[ - "black", - "brushed_titanium", # New - "gold", - "graphite", # New - "rose_gold", - "silver", - "stealth_black" # Typo in spec? "stealth" is common, "stealth_black" is more specific - ]] = Field(None, alias="color") - design: Optional[Literal[ - "balance", # New - "gucci", # New - "heritage", - "horizon" - ]] = Field( - - None, alias="design" - - ) + color: Optional[ + Literal[ + "black", + "brushed_titanium", # New + "gold", + "graphite", # New + "rose_gold", + "silver", + "stealth_black", # Typo in spec? "stealth" is common, "stealth_black" is more specific + ] + ] = Field(None, alias="color") + design: Optional[ + Literal["balance", "gucci", "heritage", "horizon"] # New # New + ] = Field(None, alias="design") firmware_version: Optional[str] = Field(None, alias="firmware_version") - hardware_type: Optional[Literal[ - "gen1", - "gen2", - "gen2m", - "gen3" - ]] = Field( - + hardware_type: Optional[Literal["gen1", "gen2", "gen2m", "gen3"]] = 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" - - ) + 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/src/oura_api_client/models/session.py similarity index 72% rename from oura_api_client/models/session.py rename to src/oura_api_client/models/session.py index e7a7fd0..95620b9 100644 --- a/oura_api_client/models/session.py +++ b/src/oura_api_client/models/session.py @@ -9,9 +9,7 @@ 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", @@ -24,45 +22,37 @@ class SessionModel(BaseModel): "session_other", # New type "sleep_sound", # New type "timer", # New type - "workout" + "workout", ] # Optional fields based on typical session data - mood: Optional[Literal[ - "bad", - "good", - "great", - "okay", - "poor", - "sensory_other", # New mood - "stressful", - "thankful", - "tired", - "undefined" - ]] = None + mood: Optional[ + Literal[ + "bad", + "good", + "great", + "okay", + "poor", + "sensory_other", # New mood + "stressful", + "thankful", + "tired", + "undefined", + ] + ] = None heart_rate: Optional[str] = Field( - None, alias="heart_rate" - ) # Assuming string, adjust if complex + 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" - - ) + 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/src/oura_api_client/models/sleep.py similarity index 94% rename from oura_api_client/models/sleep.py rename to src/oura_api_client/models/sleep.py index dd1350b..832b68e 100644 --- a/oura_api_client/models/sleep.py +++ b/src/oura_api_client/models/sleep.py @@ -5,6 +5,7 @@ class SleepContributors(BaseModel): """Sleep contributors model for sleep data.""" + deep_sleep: Optional[int] = Field(None, alias="deep_sleep") efficiency: Optional[int] = Field(None, alias="efficiency") latency: Optional[int] = Field(None, alias="latency") @@ -16,6 +17,7 @@ class SleepContributors(BaseModel): class ReadinessContributors(BaseModel): """Readiness contributors model for sleep data.""" + 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") @@ -52,10 +54,14 @@ class SleepModel(BaseModel): 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") - sleep_algorithm_version: Optional[str] = Field(None, alias="sleep_algorithm_version") + sleep_algorithm_version: Optional[str] = Field( + None, alias="sleep_algorithm_version" + ) temperature_delta: Optional[float] = Field(None, alias="temperature_delta") temperature_deviation: Optional[float] = Field(None, alias="temperature_deviation") - temperature_trend_deviation: Optional[float] = Field(None, alias="temperature_trend_deviation") + temperature_trend_deviation: Optional[float] = Field( + None, alias="temperature_trend_deviation" + ) 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") diff --git a/oura_api_client/models/sleep_time.py b/src/oura_api_client/models/sleep_time.py similarity index 83% rename from oura_api_client/models/sleep_time.py rename to src/oura_api_client/models/sleep_time.py index 8e2a28c..1672dcf 100644 --- a/oura_api_client/models/sleep_time.py +++ b/src/oura_api_client/models/sleep_time.py @@ -8,24 +8,22 @@ class SleepTimeWindow(BaseModel): day_light_saving_time: Optional[int] = Field( - None, alias="day_light_saving_time" - ) # New + 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): # Based on common sleep recommendation data, specific fields might vary # For now, keeping it simple. If a detailed spec is available, adjust. - recommendation: Optional[str] = None # e.g., "go_to_bed_earlier", "maintain_consistent_schedule" + recommendation: Optional[str] = ( + None # e.g., "go_to_bed_earlier", "maintain_consistent_schedule" + ) # Could also include specific time recommendations if the API provides them: # recommended_bedtime_start: Optional[datetime] = None # recommended_bedtime_end: Optional[datetime] = None @@ -41,25 +39,15 @@ 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" - ) + 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/src/oura_api_client/models/tag.py similarity index 89% rename from oura_api_client/models/tag.py rename to src/oura_api_client/models/tag.py index 983b89a..9d653bb 100644 --- a/oura_api_client/models/tag.py +++ b/src/oura_api_client/models/tag.py @@ -7,7 +7,9 @@ class TagModel(BaseModel): id: str day: date text: Optional[str] = None # Optional based on OpenAPI spec - timestamp: datetime # Changed from Optional[datetime] to datetime as it's usually present + 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, diff --git a/oura_api_client/models/time_series.py b/src/oura_api_client/models/time_series.py similarity index 55% rename from oura_api_client/models/time_series.py rename to src/oura_api_client/models/time_series.py index d8bbd7c..cdbce82 100644 --- a/oura_api_client/models/time_series.py +++ b/src/oura_api_client/models/time_series.py @@ -6,20 +6,29 @@ class TimeSeriesData(BaseModel): """ Time series data structure for various Oura metrics. - + This model represents time-series data with a consistent structure across different endpoints. The timestamp is automatically converted from ISO 8601 format to Unix timestamp for easier programmatic use. """ - interval: int = Field(..., description="Interval in seconds between the sampled items.") - items: List[Optional[float]] = Field(..., description="Recorded sample items. Null values indicate missing data points.") - timestamp: int = Field(..., description="Unix timestamp (seconds since epoch) when the sample recording started.") - - @field_validator('timestamp', mode='before') + + interval: int = Field( + ..., description="Interval in seconds between the sampled items." + ) + items: List[Optional[float]] = Field( + ..., + description="Recorded sample items. Null values indicate missing data points.", + ) + timestamp: int = Field( + ..., + description="Unix timestamp (seconds since epoch) when the sample recording started.", + ) + + @field_validator("timestamp", mode="before") @classmethod def parse_timestamp(cls, v): """Convert ISO 8601 timestamp string to unix timestamp.""" if isinstance(v, str): - dt = datetime.fromisoformat(v.replace('Z', '+00:00')) + dt = datetime.fromisoformat(v.replace("Z", "+00:00")) return int(dt.timestamp()) return v diff --git a/oura_api_client/models/vo2_max.py b/src/oura_api_client/models/vo2_max.py similarity index 100% rename from oura_api_client/models/vo2_max.py rename to src/oura_api_client/models/vo2_max.py diff --git a/oura_api_client/models/webhook.py b/src/oura_api_client/models/webhook.py similarity index 83% rename from oura_api_client/models/webhook.py rename to src/oura_api_client/models/webhook.py index c207241..282d4fb 100644 --- a/oura_api_client/models/webhook.py +++ b/src/oura_api_client/models/webhook.py @@ -33,63 +33,32 @@ 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" - - - -) + 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 + 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/src/oura_api_client/models/workout.py similarity index 83% rename from oura_api_client/models/workout.py rename to src/oura_api_client/models/workout.py index 37b8fb7..823cee2 100644 --- a/oura_api_client/models/workout.py +++ b/src/oura_api_client/models/workout.py @@ -1,6 +1,7 @@ from pydantic import BaseModel, Field from typing import List, Optional from datetime import date, datetime # Added date + # WorkoutIntensity and WorkoutSource are enums, but Pydantic uses Literal for this from typing import Literal @@ -14,10 +15,7 @@ class WorkoutModel(BaseModel): end_datetime: datetime = Field(alias="end_datetime") energy: Optional[float] = Field(None, alias="energy") # Kilojoules intensity: Literal[ - "easy", - "hard", - "moderate", - "restorative" # New based on common workout apps + "easy", "hard", "moderate", "restorative" # New based on common workout apps ] label: Optional[str] = None source: Literal[ @@ -27,11 +25,9 @@ class WorkoutModel(BaseModel): "health_connect", # New based on Android ecosystem "manual", "strava", # New based on common integrations - "oura_app" # New for workouts logged directly in Oura + "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: diff --git a/oura_api_client/utils/__init__.py b/src/oura_api_client/utils/__init__.py similarity index 92% rename from oura_api_client/utils/__init__.py rename to src/oura_api_client/utils/__init__.py index 4382d17..03c6e0b 100644 --- a/oura_api_client/utils/__init__.py +++ b/src/oura_api_client/utils/__init__.py @@ -9,5 +9,5 @@ "RetryConfig", "retry_with_backoff", "should_retry", - "exponential_backoff" + "exponential_backoff", ] diff --git a/oura_api_client/utils/query_params.py b/src/oura_api_client/utils/query_params.py similarity index 96% rename from oura_api_client/utils/query_params.py rename to src/oura_api_client/utils/query_params.py index e09ac59..5974640 100644 --- a/oura_api_client/utils/query_params.py +++ b/src/oura_api_client/utils/query_params.py @@ -6,10 +6,10 @@ def convert_date_to_string(date_param: Optional[Union[str, date]]) -> Optional[str]: """Convert a date parameter to ISO format string if it's a date object. - + Args: date_param: Date parameter that can be a string, date object, or None - + Returns: ISO format date string or None """ @@ -25,18 +25,18 @@ def build_query_params( **kwargs: Any ) -> Dict[str, Any]: """Build query parameters dictionary for API requests. - + This function handles common query parameter patterns: - Converts date objects to ISO format strings - Filters out None values - Supports additional parameters via kwargs - + Args: start_date: Start date for filtering (string or date object) end_date: End date for filtering (string or date object) next_token: Token for pagination **kwargs: Additional query parameters - + Returns: Dictionary of query parameters with None values filtered out """ @@ -44,8 +44,8 @@ def build_query_params( "start_date": convert_date_to_string(start_date), "end_date": convert_date_to_string(end_date), "next_token": next_token, - **kwargs + **kwargs, } - + # Filter out None values return {k: v for k, v in params.items() if v is not None} diff --git a/oura_api_client/utils/retry.py b/src/oura_api_client/utils/retry.py similarity index 83% rename from oura_api_client/utils/retry.py rename to src/oura_api_client/utils/retry.py index 827b7ad..d513dc1 100644 --- a/oura_api_client/utils/retry.py +++ b/src/oura_api_client/utils/retry.py @@ -3,57 +3,66 @@ import time import random from typing import Callable -from ..exceptions import OuraRateLimitError, OuraServerError, OuraConnectionError, OuraTimeoutError +from ..exceptions import ( + OuraRateLimitError, + OuraServerError, + OuraConnectionError, + OuraTimeoutError, +) -def exponential_backoff(attempt: int, base_delay: float = 1.0, max_delay: float = 60.0, jitter: bool = True) -> float: +def exponential_backoff( + attempt: int, base_delay: float = 1.0, max_delay: float = 60.0, jitter: bool = True +) -> float: """Calculate exponential backoff delay. - + Args: attempt: Current attempt number (0-based) base_delay: Base delay in seconds max_delay: Maximum delay in seconds jitter: Whether to add random jitter - + Returns: Delay in seconds """ - delay = base_delay * (2 ** attempt) + delay = base_delay * (2**attempt) delay = min(delay, max_delay) - + if jitter: # Add ±25% jitter jitter_range = delay * 0.25 delay += random.uniform(-jitter_range, jitter_range) - + return max(0, delay) def should_retry(exception: Exception, attempt: int, max_retries: int) -> bool: """Determine if an exception should trigger a retry. - + Args: exception: The exception that occurred attempt: Current attempt number (0-based) max_retries: Maximum number of retries allowed - + Returns: True if should retry, False otherwise """ if attempt >= max_retries: return False - + # Retry on specific transient errors if isinstance(exception, (OuraServerError, OuraConnectionError, OuraTimeoutError)): return True - + # Retry on rate limit errors if retry_after is reasonable if isinstance(exception, OuraRateLimitError): if exception.retry_after and exception.retry_after <= 300: # Max 5 minutes return True - elif not exception.retry_after: # No retry-after header, use exponential backoff + elif ( + not exception.retry_after + ): # No retry-after header, use exponential backoff return True - + return False @@ -61,70 +70,75 @@ def retry_with_backoff( max_retries: int = 3, base_delay: float = 1.0, max_delay: float = 60.0, - jitter: bool = True + jitter: bool = True, ): """Decorator factory to add retry logic with exponential backoff. - + Args: max_retries: Maximum number of retries base_delay: Base delay for exponential backoff max_delay: Maximum delay between retries jitter: Whether to add random jitter - + Returns: Decorator function """ + def decorator(func: Callable) -> Callable: """Actual decorator that wraps the function. - + Args: func: Function to wrap - + Returns: Wrapped function with retry logic """ + def wrapper(*args, **kwargs): last_exception = None - + for attempt in range(max_retries + 1): # +1 for initial attempt try: return func(*args, **kwargs) except Exception as e: last_exception = e - + if not should_retry(e, attempt, max_retries): raise - + # Calculate delay if isinstance(e, OuraRateLimitError) and e.retry_after: delay = e.retry_after else: - delay = exponential_backoff(attempt, base_delay, max_delay, jitter) - + delay = exponential_backoff( + attempt, base_delay, max_delay, jitter + ) + # Wait before retry if delay > 0: time.sleep(delay) - + # If we get here, all retries failed raise last_exception - + return wrapper + return decorator class RetryConfig: """Configuration for retry behavior.""" - + def __init__( self, max_retries: int = 3, base_delay: float = 1.0, max_delay: float = 60.0, jitter: bool = True, - enabled: bool = True + enabled: bool = True, ): """Initialize retry configuration. - + Args: max_retries: Maximum number of retries base_delay: Base delay for exponential backoff diff --git a/oura_client.py b/src/oura_client.py similarity index 100% rename from oura_client.py rename to src/oura_client.py diff --git a/parse_openapi.py b/src/parse_openapi.py similarity index 72% rename from parse_openapi.py rename to src/parse_openapi.py index d56f84a..e5829f7 100644 --- a/parse_openapi.py +++ b/src/parse_openapi.py @@ -9,59 +9,81 @@ def _parse_method_details(method_details): "summary": method_details.get("summary"), "operationId": method_details.get("operationId"), "parameters": [], - "responses": {} + "responses": {}, } - + # Parse parameters - if "parameters" in method_details and isinstance(method_details["parameters"], list): + if "parameters" in method_details and isinstance( + method_details["parameters"], list + ): for param in method_details["parameters"]: - method_data["parameters"].append({ - "name": param.get("name"), - "in": param.get("in"), - "required": param.get("required"), - "schema": param.get("schema") - }) - + method_data["parameters"].append( + { + "name": param.get("name"), + "in": param.get("in"), + "required": param.get("required"), + "schema": param.get("schema"), + } + ) + # Parse responses if "responses" in method_details and isinstance(method_details["responses"], dict): for status_code, response_details in method_details["responses"].items(): method_data["responses"][status_code] = { "description": response_details.get("description"), - "content": response_details.get("content", {}).get("application/json", {}).get("schema") + "content": response_details.get("content", {}) + .get("application/json", {}) + .get("schema"), } - + return method_data def _parse_paths(spec): """Parse paths section of OpenAPI spec.""" paths_data = {} - + if "paths" not in spec or not isinstance(spec["paths"], dict): - print("Warning: 'paths' attribute not found or is not a dictionary in the OpenAPI spec.") + print( + "Warning: 'paths' attribute not found or is not a dictionary in the OpenAPI spec." + ) return paths_data - + for path_string, path_item in spec["paths"].items(): paths_data[path_string] = {} for method, method_details in path_item.items(): - if method.upper() in ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]: - paths_data[path_string][method.upper()] = _parse_method_details(method_details) - + if method.upper() in [ + "GET", + "POST", + "PUT", + "DELETE", + "PATCH", + "OPTIONS", + "HEAD", + ]: + paths_data[path_string][method.upper()] = _parse_method_details( + method_details + ) + return paths_data def _parse_schemas(spec): """Parse component schemas section of OpenAPI spec.""" components_schemas_data = {} - - if ("components" in spec and - "schemas" in spec["components"] and - isinstance(spec["components"]["schemas"], dict)): + + if ( + "components" in spec + and "schemas" in spec["components"] + and isinstance(spec["components"]["schemas"], dict) + ): for schema_name, schema_details in spec["components"]["schemas"].items(): components_schemas_data[schema_name] = schema_details else: - print("Warning: 'components.schemas' attribute not found or is not a dictionary in the OpenAPI spec.") - + print( + "Warning: 'components.schemas' attribute not found or is not a dictionary in the OpenAPI spec." + ) + return components_schemas_data diff --git a/reference_spec.py b/src/reference_spec.py similarity index 86% rename from reference_spec.py rename to src/reference_spec.py index 6cb0a18..e80abd7 100644 --- a/reference_spec.py +++ b/src/reference_spec.py @@ -1,7 +1,7 @@ -#** Final Oura API V2 Blueprint for Python -#* This document provides a comprehensive, redesigned blueprint for the Oura API V2, engineered for a general-purpose Python library. It is the result of a critical analysis of the official OpenAPI specification and incorporates feedback to create robust, developer-friendly data models and documentation. -#* This is a developer-focused blueprint, not a formal OpenAPI spec, designed for direct translation into Pydantic models. -# +# ** Final Oura API V2 Blueprint for Python +# * This document provides a comprehensive, redesigned blueprint for the Oura API V2, engineered for a general-purpose Python library. It is the result of a critical analysis of the official OpenAPI specification and incorporates feedback to create robust, developer-friendly data models and documentation. +# * This is a developer-focused blueprint, not a formal OpenAPI spec, designed for direct translation into Pydantic models. +# # 1. Core Data ModelsThese are the foundational Pydantic-style models. They are designed to be intuitive, type-safe, and cover the entire surface of the Oura V2 API data endpoints. from datetime import date, datetime, timedelta @@ -12,8 +12,10 @@ # ENUMERATION MODELS # =================================================================== + class ScoreContributor(str, Enum): """Enumeration for factors contributing to a readiness or sleep score.""" + ACTIVITY_BALANCE = "activity_balance" BODY_TEMPERATURE = "body_temperature" HRV_BALANCE = "hrv_balance" @@ -30,8 +32,10 @@ class ScoreContributor(str, Enum): TIMING = "timing" TOTAL_SLEEP = "total_sleep" + class ActivityLevel(str, Enum): """Enumeration for activity intensity levels.""" + NON_WEAR = "non_wear" REST = "rest" INACTIVE = "inactive" @@ -39,15 +43,19 @@ class ActivityLevel(str, Enum): MEDIUM = "medium" HIGH = "high" + class SleepPhase(str, Enum): """Enumeration for sleep phases.""" + AWAKE = "awake" LIGHT = "light" DEEP = "deep" REM = "rem" + class SessionType(str, Enum): """Enumeration for session/moment types.""" + BREATHING = "breathing" MEDITATION = "meditation" NAP = "nap" @@ -55,30 +63,38 @@ class SessionType(str, Enum): REST = "rest" BODY_STATUS = "body_status" + class WorkoutSource(str, Enum): """Enumeration for the source of a workout entry.""" + AUTODETECTED = "autodetected" CONFIRMED = "confirmed" MANUAL = "manual" WORKOUT_HEART_RATE = "workout_heart_rate" + class ResilienceLevel(str, Enum): """Enumeration for long-term resilience levels.""" + LIMITED = "limited" ADEQUATE = "adequate" SOLID = "solid" STRONG = "strong" EXCEPTIONAL = "exceptional" + class RingDesign(str, Enum): """Enumeration for Oura Ring designs.""" + BALANCE = "balance" BALANCE_DIAMOND = "balance_diamond" HERITAGE = "heritage" HORIZON = "horizon" + class RingColor(str, Enum): """Enumeration for Oura Ring colors.""" + BRUSHED_SILVER = "brushed_silver" GLOSSY_BLACK = "glossy_black" GLOSSY_GOLD = "glossy_gold" @@ -90,19 +106,24 @@ class RingColor(str, Enum): STEALTH_BLACK = "stealth_black" TITANIUM = "titanium" + class RingHardwareType(str, Enum): """Enumeration for Oura Ring hardware generations.""" + GEN1 = "gen1" GEN2 = "gen2" GEN2M = "gen2m" GEN3 = "gen3" + # =================================================================== # SHARED & REUSABLE MODELS # =================================================================== + class TimeInterval: """A reusable model to represent a time interval with a start and optional end.""" + start: datetime end: Optional[datetime] = None @@ -113,24 +134,31 @@ def duration(self) -> Optional[timedelta]: return self.end - self.start return None + class TimeSeries: """A generic model for time-series data like heart rate or HRV.""" - timestamp: datetime # Start time of the first sample. - interval_seconds: int # Number of seconds between items. - items: List[Optional[float]] # The list of sampled data points. + + timestamp: datetime # Start time of the first sample. + interval_seconds: int # Number of seconds between items. + items: List[Optional[float]] # The list of sampled data points. + class HeartRateSample: """Represents a single heart rate measurement at a point in time.""" + bpm: int source: str timestamp: datetime + # =================================================================== # PRIMARY DATA MODELS # =================================================================== + class PersonalInfo: """Represents the user's core biological and demographic information.""" + id: str age: Optional[int] = None weight_kg: Optional[float] = None @@ -138,8 +166,10 @@ class PersonalInfo: biological_sex: Optional[str] = None email: Optional[str] = None + class RingConfiguration: """Represents the configuration and hardware details of a user's Oura Ring.""" + id: str color: Optional[RingColor] = None design: Optional[RingDesign] = None @@ -148,8 +178,10 @@ class RingConfiguration: size: Optional[int] = None set_up_at: Optional[datetime] = None + class DailyReadiness: """Represents the user's readiness score for a single day.""" + id: str day: date score: Optional[int] = None @@ -157,8 +189,10 @@ class DailyReadiness: temperature_deviation_celsius: Optional[float] = None temperature_trend_deviation_celsius: Optional[float] = None + class DailyActivity: """Represents the user's activity summary for a single day.""" + id: str day: date score: Optional[int] = None @@ -175,8 +209,10 @@ class DailyActivity: target_calories: Optional[int] = None target_meters: Optional[int] = None + class DailySleep: """Represents consolidated sleep data for a single sleep period.""" + id: str day: date bedtime: TimeInterval @@ -195,17 +231,21 @@ class DailySleep: average_hrv: Optional[int] = None hrv_timeseries: Optional[TimeSeries] = None heart_rate_timeseries: Optional[TimeSeries] = None - + + class EnhancedTag: """Represents a user-created tag for logging events, habits, or feelings.""" + id: str interval: TimeInterval tag_type_code: Optional[str] = None comment: Optional[str] = None custom_name: Optional[str] = None + class Workout: """Represents a single workout session.""" + id: str interval: TimeInterval activity: str @@ -215,8 +255,10 @@ class Workout: distance_meters: Optional[float] = None label: Optional[str] = None + class Session: """Represents a mindfulness, nap, or other session.""" + id: str interval: TimeInterval day: date @@ -225,39 +267,51 @@ class Session: heart_rate_timeseries: Optional[TimeSeries] = None hrv_timeseries: Optional[TimeSeries] = None + class DailySpo2: """Represents the user's blood oxygen saturation (SpO2) summary for a single day.""" + id: str day: date average_spo2_percentage: Optional[float] = None + class DailyStress: """Represents the user's stress and recovery summary for a single day.""" + id: str day: date stress_high_duration: Optional[timedelta] = None recovery_high_duration: Optional[timedelta] = None - + + class DailyResilience: """Represents the user's resilience summary for a single day.""" + id: str day: date level: Optional[ResilienceLevel] = None + class CardiovascularAge: """Represents the user's cardiovascular age assessment.""" + id: str day: date vascular_age: Optional[float] = None + class VO2Max: """Represents the user's VO2 Max assessment (maximal oxygen uptake).""" + id: str day: date vo2_max: Optional[float] = None + class HeartRateData: """Represents a collection of heart rate measurements over a time interval.""" + data: List[HeartRateSample] next_token: Optional[str] = None @@ -265,20 +319,21 @@ class HeartRateData: # 2. API Client DefinitionThis section defines the methods for a comprehensive client, with detailed documentation suitable for a high-quality library.# python # Base URL for all API calls: https://api.ouraring.com/v2/usercollection + class OuraApiClient: """A conceptual Python client for interacting with the optimized Oura API models.""" def get_personal_info(self) -> PersonalInfo: """ Retrieves the user's basic biological and demographic information. - + This data changes infrequently and is suitable for caching. Returns: PersonalInfo: An object containing the user's age, weight, height, etc. """ pass - + def get_ring_configurations(self) -> List[RingConfiguration]: """ Retrieves a list of all rings ever associated with the user's account. @@ -289,7 +344,9 @@ def get_ring_configurations(self) -> List[RingConfiguration]: """ pass - def get_daily_readiness(self, start_date: date, end_date: Optional[date] = None) -> List[DailyReadiness]: + def get_daily_readiness( + self, start_date: date, end_date: Optional[date] = None + ) -> List[DailyReadiness]: """ Retrieves daily readiness summaries for a given date range. @@ -302,7 +359,9 @@ def get_daily_readiness(self, start_date: date, end_date: Optional[date] = None) """ pass - def get_daily_activity(self, start_date: date, end_date: Optional[date] = None) -> List[DailyActivity]: + def get_daily_activity( + self, start_date: date, end_date: Optional[date] = None + ) -> List[DailyActivity]: """ Retrieves daily activity summaries for a given date range. @@ -315,13 +374,15 @@ def get_daily_activity(self, start_date: date, end_date: Optional[date] = None) """ pass - def get_daily_sleep(self, start_date: date, end_date: Optional[date] = None) -> List[DailySleep]: + def get_daily_sleep( + self, start_date: date, end_date: Optional[date] = None + ) -> List[DailySleep]: """ Retrieves comprehensive sleep data for a given date range. Note: A robust implementation of this method should intelligently query both the `/daily_sleep` and `/sleep` endpoints to construct the complete `DailySleep` model. - + Args: start_date: The first day of the query range. end_date: The last day of the query range. If None, queries for start_date only. @@ -331,7 +392,9 @@ def get_daily_sleep(self, start_date: date, end_date: Optional[date] = None) -> """ pass - def get_daily_spo2(self, start_date: date, end_date: Optional[date] = None) -> List[DailySpo2]: + def get_daily_spo2( + self, start_date: date, end_date: Optional[date] = None + ) -> List[DailySpo2]: """ Retrieves daily blood oxygen saturation (SpO2) summaries. @@ -344,7 +407,9 @@ def get_daily_spo2(self, start_date: date, end_date: Optional[date] = None) -> L """ pass - def get_daily_stress(self, start_date: date, end_date: Optional[date] = None) -> List[DailyStress]: + def get_daily_stress( + self, start_date: date, end_date: Optional[date] = None + ) -> List[DailyStress]: """ Retrieves daily stress and recovery summaries. @@ -357,7 +422,9 @@ def get_daily_stress(self, start_date: date, end_date: Optional[date] = None) -> """ pass - def get_daily_resilience(self, start_date: date, end_date: Optional[date] = None) -> List[DailyResilience]: + def get_daily_resilience( + self, start_date: date, end_date: Optional[date] = None + ) -> List[DailyResilience]: """ Retrieves daily resilience summaries. @@ -370,7 +437,9 @@ def get_daily_resilience(self, start_date: date, end_date: Optional[date] = None """ pass - def get_cardiovascular_age(self, start_date: date, end_date: Optional[date] = None) -> List[CardiovascularAge]: + def get_cardiovascular_age( + self, start_date: date, end_date: Optional[date] = None + ) -> List[CardiovascularAge]: """ Retrieves cardiovascular age assessments. @@ -383,7 +452,9 @@ def get_cardiovascular_age(self, start_date: date, end_date: Optional[date] = No """ pass - def get_vo2_max(self, start_date: date, end_date: Optional[date] = None) -> List[VO2Max]: + def get_vo2_max( + self, start_date: date, end_date: Optional[date] = None + ) -> List[VO2Max]: """ Retrieves VO2 Max assessments. @@ -396,7 +467,9 @@ def get_vo2_max(self, start_date: date, end_date: Optional[date] = None) -> List """ pass - def get_heart_rate(self, start_datetime: datetime, end_datetime: Optional[datetime] = None) -> HeartRateData: + def get_heart_rate( + self, start_datetime: datetime, end_datetime: Optional[datetime] = None + ) -> HeartRateData: """ Retrieves high-resolution, time-series heart rate data. @@ -409,7 +482,9 @@ def get_heart_rate(self, start_datetime: datetime, end_datetime: Optional[dateti """ pass - def get_workouts(self, start_date: date, end_date: Optional[date] = None) -> List[Workout]: + def get_workouts( + self, start_date: date, end_date: Optional[date] = None + ) -> List[Workout]: """ Retrieves user-logged workouts for a given date range. @@ -422,7 +497,9 @@ def get_workouts(self, start_date: date, end_date: Optional[date] = None) -> Lis """ pass - def get_sessions(self, start_date: date, end_date: Optional[date] = None) -> List[Session]: + def get_sessions( + self, start_date: date, end_date: Optional[date] = None + ) -> List[Session]: """ Retrieves mindfulness, nap, and other guided/unguided sessions. @@ -434,8 +511,10 @@ def get_sessions(self, start_date: date, end_date: Optional[date] = None) -> Lis List[Session]: A list of session objects. """ pass - - def get_enhanced_tags(self, start_date: date, end_date: Optional[date] = None) -> List[EnhancedTag]: + + def get_enhanced_tags( + self, start_date: date, end_date: Optional[date] = None + ) -> List[EnhancedTag]: """ Retrieves user-created tags for logging events, habits, or feelings. diff --git a/tests/test_client.py b/tests/test_client.py index c3df0ca..30bf815 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -7,50 +7,72 @@ 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 + DailyActivityResponse, + DailyActivityModel, + ActivityContributors, ) from oura_api_client.models.daily_sleep import ( - DailySleepResponse, DailySleepModel, SleepContributors as DailySleepContributors + DailySleepResponse, + DailySleepModel, + SleepContributors as DailySleepContributors, ) from oura_api_client.models.daily_readiness import ( - DailyReadinessResponse, DailyReadinessModel, ReadinessContributors as DailyReadinessContributors + DailyReadinessResponse, + DailyReadinessModel, + ReadinessContributors as DailyReadinessContributors, +) +from oura_api_client.models.sleep import ( + SleepResponse, + SleepModel, + SleepContributors, + ReadinessContributors, ) -from oura_api_client.models.sleep import SleepResponse, SleepModel, SleepContributors, ReadinessContributors 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.enhanced_tag import EnhancedTagResponse, EnhancedTagModel from oura_api_client.models.daily_spo2 import ( - DailySpO2Response, DailySpO2Model, DailySpO2AggregatedValuesModel + DailySpO2Response, + DailySpO2Model, + DailySpO2AggregatedValuesModel, ) from oura_api_client.models.sleep_time import ( - SleepTimeResponse, SleepTimeModel, SleepTimeWindow, - SleepTimeRecommendation, SleepTimeStatus + SleepTimeResponse, + SleepTimeModel, + SleepTimeWindow, + SleepTimeRecommendation, + SleepTimeStatus, ) from oura_api_client.models.rest_mode_period import ( - RestModePeriodResponse, RestModePeriodModel + RestModePeriodResponse, + RestModePeriodModel, ) # Added RestModePeriod models from oura_api_client.models.daily_stress import ( - DailyStressResponse, DailyStressModel + DailyStressResponse, + DailyStressModel, ) # Added DailyStress models from oura_api_client.models.daily_resilience import ( - DailyResilienceResponse, DailyResilienceModel + DailyResilienceResponse, + DailyResilienceModel, ) # Added DailyResilience models from oura_api_client.models.daily_cardiovascular_age import ( - DailyCardiovascularAgeResponse, DailyCardiovascularAgeModel + DailyCardiovascularAgeResponse, + DailyCardiovascularAgeModel, ) # Added DailyCardiovascularAge models from oura_api_client.models.vo2_max import ( - Vo2MaxResponse, Vo2MaxModel + Vo2MaxResponse, + Vo2MaxModel, ) # Added Vo2Max models import requests from oura_api_client.exceptions import ( - OuraNotFoundError, OuraRateLimitError, - OuraClientError, OuraConnectionError + OuraNotFoundError, + OuraRateLimitError, + OuraClientError, + OuraConnectionError, ) + class TestOuraClient(unittest.TestCase): """Test the OuraClient class.""" @@ -61,9 +83,7 @@ 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) @@ -95,11 +115,7 @@ 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, } @@ -127,9 +143,11 @@ def test_get_heart_rate(self, mock_get): timeout=30.0, ) + if __name__ == "__main__": unittest.main() + class TestDailyActivity(unittest.TestCase): def setUp(self): @@ -149,10 +167,7 @@ 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() # Simulate no HTTP error @@ -168,20 +183,14 @@ def test_get_daily_activity_documents(self, mock_get): daily_activity_response = ( self.client.daily_activity.get_daily_activity_documents( - start_date=start_date, - end_date=end_date, - next_token="test_token" + 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" - ) + 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] @@ -189,7 +198,7 @@ def test_get_daily_activity_documents(self, mock_get): expected_url = f"{base_url}/usercollection/daily_activity" self.assertTrue(actual_call_url.endswith(expected_url)) - called_params = mock_get.call_args[1]['params'] + called_params = mock_get.call_args[1]["params"] expected_params = { "start_date": start_date_str, "end_date": end_date_str, @@ -223,7 +232,7 @@ def test_get_daily_activity_documents_with_string_dates(self, mock_get): expected_url = f"{base_url}/usercollection/daily_activity" self.assertTrue(actual_call_url.endswith(expected_url)) - called_params = mock_get.call_args[1]['params'] + 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) @@ -262,7 +271,7 @@ def test_get_daily_activity_document(self, mock_get): "met": { "interval": 5, "items": [1.5, 2.0, 1.8, 2.2], - "timestamp": "2024-03-10T12:00:00+00:00" + "timestamp": "2024-03-10T12:00:00+00:00", }, "meters_to_target": 1000, "non_wear_time": 300, @@ -282,20 +291,24 @@ def test_get_daily_activity_document(self, mock_get): 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 + 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) + 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}/usercollection/daily_activity/{document_id}" self.assertTrue(actual_call_url.endswith(expected_url)) - called_params = mock_get.call_args[1]['params'] + called_params = mock_get.call_args[1]["params"] self.assertEqual(called_params, None) @patch("requests.get") @@ -303,7 +316,10 @@ def test_get_daily_activity_document_error(self, mock_get): mock_get.side_effect = OuraConnectionError("API error") document_id = "test_document_id" with self.assertRaises(OuraConnectionError): - self.client.daily_activity.get_daily_activity_document(document_id=document_id) + self.client.daily_activity.get_daily_activity_document( + document_id=document_id + ) + class TestDailySleep(unittest.TestCase): def setUp(self): @@ -353,20 +369,15 @@ 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, - DailySleepContributors + daily_sleep_response.data[0].contributors, DailySleepContributors ) self.assertEqual(daily_sleep_response.next_token, "next_sleep_token") @@ -451,17 +462,15 @@ 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, DailySleepContributors - ) + self.assertIsInstance(daily_sleep_document.contributors, DailySleepContributors) self.assertEqual(daily_sleep_document.score, 85) self.assertEqual( daily_sleep_document.bedtime_end, - datetime.fromisoformat("2024-03-11T07:00:00+00:00") + 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") + datetime.fromisoformat("2024-03-10T22:00:00+00:00"), ) mock_get.assert_called_once_with( @@ -478,6 +487,7 @@ def test_get_daily_sleep_document_error(self, mock_get): with self.assertRaises(OuraConnectionError): self.client.daily_sleep.get_daily_sleep_document(document_id=document_id) + class TestDailyReadiness(unittest.TestCase): def setUp(self): @@ -516,22 +526,17 @@ def test_get_daily_readiness_documents(self, mock_get): self.client.daily_readiness.get_daily_readiness_documents( start_date=start_date, end_date=end_date, - next_token="test_readiness_token" + 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], DailyReadinessModel - ) - self.assertIsInstance( - daily_readiness_response.data[0].contributors, - DailyReadinessContributors - ) - self.assertEqual( - daily_readiness_response.next_token, "next_readiness_token" + daily_readiness_response.data[0].contributors, DailyReadinessContributors ) + self.assertEqual(daily_readiness_response.next_token, "next_readiness_token") mock_get.assert_called_once_with( f"{self.client.BASE_URL}/usercollection/daily_readiness", @@ -620,13 +625,11 @@ def test_get_daily_readiness_document(self, mock_get): self.assertIsInstance(daily_readiness_document, DailyReadinessModel) self.assertEqual(daily_readiness_document.id, document_id) self.assertIsInstance( - daily_readiness_document.contributors, - DailyReadinessContributors + daily_readiness_document.contributors, DailyReadinessContributors ) self.assertEqual(daily_readiness_document.score, 78) self.assertEqual( - daily_readiness_document.activity_class_5_min, - "some_activity_class" + 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) @@ -647,6 +650,7 @@ def test_get_daily_readiness_document_error(self, mock_get): document_id=document_id ) + class TestSleep(unittest.TestCase): def setUp(self): @@ -656,17 +660,24 @@ def setUp(self): 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, - "restfulness": 75, "timing": 85, "total_sleep": 95, + "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, + "activity_balance": 60, + "body_temperature": 70, "hrv_balance": 80, - "previous_day_activity": 90, "previous_night": 50, + "previous_day_activity": 90, + "previous_night": 50, "recovery_index": 65, - "resting_heart_rate": 75, "sleep_balance": 85, + "resting_heart_rate": 75, + "sleep_balance": 85, } mock_data = [ { @@ -691,10 +702,7 @@ 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 @@ -706,23 +714,14 @@ 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( @@ -739,12 +738,14 @@ def test_get_sleep_documents(self, mock_get): @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 @@ -795,9 +796,7 @@ 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) @@ -806,7 +805,7 @@ def test_get_sleep_document(self, mock_get): self.assertEqual(sleep_document.score, 88) self.assertEqual( sleep_document.bedtime_end, - datetime.fromisoformat("2024-03-11T07:30:00+00:00") + datetime.fromisoformat("2024-03-11T07:30:00+00:00"), ) mock_get.assert_called_once_with( @@ -823,6 +822,7 @@ def test_get_sleep_document_error(self, mock_get): with self.assertRaises(OuraConnectionError): self.client.sleep.get_sleep_document(document_id=document_id) + class TestSession(unittest.TestCase): def setUp(self): @@ -861,9 +861,7 @@ 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) @@ -951,7 +949,7 @@ def test_get_session_document(self, mock_get): self.assertEqual(session_document.duration, 2700) self.assertEqual( session_document.start_datetime, - datetime.fromisoformat("2024-03-10T15:00:00+00:00") + datetime.fromisoformat("2024-03-10T15:00:00+00:00"), ) mock_get.assert_called_once_with( @@ -968,6 +966,7 @@ def test_get_session_document_error(self, mock_get): with self.assertRaises(OuraConnectionError): self.client.session.get_session_document(document_id=document_id) + class TestTag(unittest.TestCase): def setUp(self): @@ -1001,9 +1000,7 @@ 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) @@ -1074,16 +1071,13 @@ 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") + tag_document.timestamp, datetime.fromisoformat("2024-03-10T11:00:00+00:00") ) mock_get.assert_called_once_with( @@ -1100,6 +1094,7 @@ def test_get_tag_document_error(self, mock_get): with self.assertRaises(OuraConnectionError): self.client.tag.get_tag_document(document_id=document_id) + class TestWorkout(unittest.TestCase): def setUp(self): @@ -1141,9 +1136,7 @@ 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) @@ -1235,7 +1228,7 @@ def test_get_workout_document(self, mock_get): self.assertEqual(workout_document.source, "apple_health") self.assertEqual( workout_document.start_datetime, - datetime.fromisoformat("2024-03-10T12:00:00+00:00") + datetime.fromisoformat("2024-03-10T12:00:00+00:00"), ) mock_get.assert_called_once_with( @@ -1252,6 +1245,7 @@ def test_get_workout_document_error(self, mock_get): with self.assertRaises(OuraConnectionError): self.client.workout.get_workout_document(document_id=document_id) + class TestEnhancedTag(unittest.TestCase): def setUp(self): @@ -1267,19 +1261,19 @@ def test_get_enhanced_tag_documents(self, mock_get): "end_time": "2024-03-12T00:00:00+00:00", "start_day": "2024-03-10", "end_day": "2024-03-12", - "comment": "Feeling under the weather." + "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!" + "comment": "Beach time!", }, ] mock_response_json = { "data": mock_data, - "next_token": "next_enhanced_tag_token" + "next_token": "next_enhanced_tag_token", } mock_response = MagicMock() mock_response.raise_for_status.return_value = None @@ -1294,21 +1288,15 @@ def test_get_enhanced_tag_documents(self, mock_get): 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" + 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}/usercollection/enhanced_tag", @@ -1368,7 +1356,7 @@ def test_get_enhanced_tag_document(self, mock_get): "end_time": "2024-03-10T18:00:00+00:00", "start_day": "2024-03-10", "end_day": "2024-03-10", - "comment": "Tough day at work." + "comment": "Tough day at work.", } mock_response = MagicMock() mock_response.raise_for_status.return_value = None @@ -1386,11 +1374,9 @@ def test_get_enhanced_tag_document(self, mock_get): 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) + 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}/usercollection/enhanced_tag/{document_id}", @@ -1406,6 +1392,7 @@ def test_get_enhanced_tag_document_error(self, mock_get): with self.assertRaises(OuraConnectionError): self.client.enhanced_tag.get_enhanced_tag_document(document_id=document_id) + class TestDailySpo2(unittest.TestCase): def setUp(self): @@ -1419,19 +1406,16 @@ def test_get_daily_spo2_documents(self, mock_get): "day": "2024-03-10", "spo2_percentage": 97.5, "aggregated_values": {"average": 97.5}, - "timestamp": "2024-03-11T00:00:00+00:00" + "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" + "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 @@ -1443,9 +1427,7 @@ 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) @@ -1455,7 +1437,7 @@ def test_get_daily_spo2_documents(self, mock_get): if daily_spo2_response.data[0].aggregated_values: self.assertIsInstance( daily_spo2_response.data[0].aggregated_values, - DailySpO2AggregatedValuesModel + DailySpO2AggregatedValuesModel, ) self.assertEqual(daily_spo2_response.next_token, "next_spo2_token") self.assertEqual(daily_spo2_response.data[0].spo2_percentage, 97.5) @@ -1478,7 +1460,7 @@ def test_get_daily_spo2_documents_with_string_dates(self, mock_get): "id": "spo2_str_date", "day": "2024-03-10", "aggregated_values": {"average": 96.0}, - "timestamp": "2024-03-11T00:00:00+00:00" + "timestamp": "2024-03-11T00:00:00+00:00", } ] mock_response_json = {"data": mock_data, "next_token": None} @@ -1516,7 +1498,7 @@ def test_get_daily_spo2_document(self, mock_get): "day": "2024-03-10", "spo2_percentage": 98.2, "aggregated_values": {"average": 98.2}, - "timestamp": "2024-03-11T00:00:00+00:00" + "timestamp": "2024-03-11T00:00:00+00:00", } mock_response = MagicMock() mock_response.raise_for_status.return_value = None @@ -1535,7 +1517,7 @@ def test_get_daily_spo2_document(self, mock_get): 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") + datetime.fromisoformat("2024-03-11T00:00:00+00:00"), ) mock_get.assert_called_once_with( @@ -1552,6 +1534,7 @@ def test_get_daily_spo2_document_error(self, mock_get): with self.assertRaises(OuraConnectionError): self.client.daily_spo2.get_daily_spo2_document(document_id=document_id) + class TestSleepTime(unittest.TestCase): def setUp(self): @@ -1566,31 +1549,23 @@ def test_get_sleep_time_documents(self, mock_get): "optimal_bedtime": { "start_offset": -1800, "end_offset": 3600, - "day_light_saving_time": 0 + "day_light_saving_time": 0, }, "recommendation": {"recommendation": "go_to_bed_earlier"}, "status": {"status": "slightly_late"}, - "timestamp": "2024-03-10T04:00:00+00:00" + "timestamp": "2024-03-10T04:00:00+00:00", }, { "id": "st_2", "day": "2024-03-11", - # Missing day_light_saving_time to test Optional - "optimal_bedtime": { - "start_offset": -1500, - "end_offset": 3900 - }, - "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" + "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 @@ -1602,9 +1577,7 @@ 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) @@ -1612,19 +1585,14 @@ def test_get_sleep_time_documents(self, mock_get): 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 + sleep_time_response.data[0].optimal_bedtime, SleepTimeWindow ) if sleep_time_response.data[0].recommendation: self.assertIsInstance( - sleep_time_response.data[0].recommendation, - SleepTimeRecommendation + 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)) @@ -1646,7 +1614,7 @@ def test_get_sleep_time_documents_with_string_dates(self, mock_get): "id": "st_str_date", "day": "2024-03-10", "optimal_bedtime": {"start_offset": -1800, "end_offset": 3600}, - "timestamp": "2024-03-10T04:00:00+00:00" + "timestamp": "2024-03-10T04:00:00+00:00", } ] mock_response_json = {"data": mock_data, "next_token": None} @@ -1685,11 +1653,11 @@ def test_get_sleep_time_document(self, mock_get): "optimal_bedtime": { "start_offset": -1800, "end_offset": 3600, - "day_light_saving_time": 0 + "day_light_saving_time": 0, }, "recommendation": {"recommendation": "go_to_bed_earlier"}, "status": {"status": "slightly_late"}, - "timestamp": "2024-03-10T04:00:00+00:00" + "timestamp": "2024-03-10T04:00:00+00:00", } mock_response = MagicMock() mock_response.raise_for_status.return_value = None @@ -1704,21 +1672,16 @@ def test_get_sleep_time_document(self, mock_get): 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" + 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.status.status, "slightly_late") self.assertEqual( sleep_time_document.timestamp, - datetime.fromisoformat("2024-03-10T04:00:00+00:00") + datetime.fromisoformat("2024-03-10T04:00:00+00:00"), ) mock_get.assert_called_once_with( @@ -1733,11 +1696,14 @@ 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). - mock_get.side_effect = requests.exceptions.ConnectionError("API error or Not Found") + mock_get.side_effect = requests.exceptions.ConnectionError( + "API error or Not Found" + ) document_id = "test_st_single_error" with self.assertRaises(OuraConnectionError): self.client.sleep_time.get_sleep_time_document(document_id=document_id) + class TestRestModePeriod(unittest.TestCase): def setUp(self): @@ -1775,21 +1741,15 @@ def test_get_rest_mode_period_documents(self, mock_get): 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" + 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}/usercollection/rest_mode_period", @@ -1864,13 +1824,10 @@ def test_get_rest_mode_period_document(self, mock_get): 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") + rmp_document.start_time, datetime.fromisoformat("2024-03-10T10:00:00+00:00") ) mock_get.assert_called_once_with( @@ -1889,6 +1846,7 @@ def test_get_rest_mode_period_document_error(self, mock_get): document_id=document_id ) + class TestDailyStress(unittest.TestCase): def setUp(self): @@ -1920,9 +1878,7 @@ 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}/usercollection/daily_stress", headers=self.client.headers, @@ -1972,9 +1928,7 @@ 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}/usercollection/daily_stress", headers=self.client.headers, @@ -1994,10 +1948,7 @@ def test_get_daily_stress_documents_success(self, mock_get): "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 @@ -2030,7 +1981,7 @@ def test_get_daily_stress_documents_api_error_400(self, mock_get): mock_response.reason = "Client Error" mock_response.json.return_value = {"error": "400 Client Error"} mock_get.return_value = mock_response - + with self.assertRaises(OuraClientError): self.client.daily_stress.get_daily_stress_documents() @@ -2043,7 +1994,7 @@ def test_get_daily_stress_documents_api_error_429(self, mock_get): mock_response.reason = "Too Many Requests" mock_response.json.return_value = {"error": "429 Client Error"} mock_get.return_value = mock_response - + with self.assertRaises(OuraRateLimitError): self.client.daily_stress.get_daily_stress_documents() @@ -2092,6 +2043,7 @@ def test_get_daily_stress_document_not_found_404(self, mock_get): with self.assertRaises(OuraNotFoundError): self.client.daily_stress.get_daily_stress_document(document_id) + class TestDailyResilience(unittest.TestCase): def setUp(self): @@ -2203,10 +2155,7 @@ def test_get_daily_resilience_documents_success(self, mock_get): "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 @@ -2292,9 +2241,8 @@ def test_get_daily_resilience_document_not_found_404(self, mock_get): mock_get.return_value = mock_response with self.assertRaises(OuraNotFoundError): - 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): @@ -2405,10 +2353,7 @@ def test_get_daily_cardiovascular_age_documents_success(self, mock_get): "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 @@ -2489,6 +2434,7 @@ def test_get_daily_cardiovascular_age_document_not_found_404(self, mock_get): document_id ) + class TestVo2Max(unittest.TestCase): def setUp(self): @@ -2589,18 +2535,13 @@ 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] @@ -2646,8 +2587,7 @@ def test_get_vo2_max_document_success(self, mock_get): 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") + response.timestamp, datetime.fromisoformat("2024-04-11T11:00:00+00:00") ) self.assertEqual(response.vo2_max, 36.2) diff --git a/tests/test_error_handling.py b/tests/test_error_handling.py index 947a6d5..dfea5a2 100644 --- a/tests/test_error_handling.py +++ b/tests/test_error_handling.py @@ -14,7 +14,7 @@ OuraClientError, OuraConnectionError, OuraTimeoutError, - create_api_error + create_api_error, ) from oura_api_client.utils import RetryConfig, exponential_backoff, should_retry @@ -33,10 +33,7 @@ def test_oura_api_error(self): def test_oura_rate_limit_error(self): """Test OuraRateLimitError with retry_after.""" error = OuraRateLimitError( - "Rate limit exceeded", - status_code=429, - endpoint="/test", - retry_after=60 + "Rate limit exceeded", status_code=429, endpoint="/test", retry_after=60 ) self.assertEqual(error.retry_after, 60) self.assertEqual(error.status_code, 429) @@ -47,7 +44,7 @@ def test_create_api_error_with_json_response(self): mock_response.status_code = 401 mock_response.reason = "Unauthorized" mock_response.json.return_value = {"error": "Invalid token"} - + error = create_api_error(mock_response, "/test") self.assertIsInstance(error, OuraAuthenticationError) self.assertEqual(error.message, "Invalid token") @@ -60,7 +57,7 @@ def test_create_api_error_without_json(self): mock_response.status_code = 500 mock_response.reason = "Internal Server Error" mock_response.json.side_effect = ValueError("No JSON") - + error = create_api_error(mock_response, "/test") self.assertIsInstance(error, OuraServerError) self.assertEqual(error.message, "HTTP 500: Internal Server Error") @@ -72,7 +69,7 @@ def test_create_api_error_with_retry_after(self): mock_response.reason = "Too Many Requests" mock_response.headers = {"Retry-After": "120"} mock_response.json.return_value = {"error": "Rate limit exceeded"} - + error = create_api_error(mock_response, "/test") self.assertIsInstance(error, OuraRateLimitError) self.assertEqual(error.retry_after, 120) @@ -90,14 +87,14 @@ def test_create_api_error_status_mapping(self): (502, OuraServerError), (503, OuraServerError), ] - + for status_code, expected_type in test_cases: mock_response = MagicMock() mock_response.status_code = status_code mock_response.reason = f"Status {status_code}" mock_response.json.side_effect = ValueError() mock_response.headers = {} - + error = create_api_error(mock_response) self.assertIsInstance(error, expected_type) @@ -112,17 +109,16 @@ def test_exponential_backoff(self): self.assertEqual(exponential_backoff(1, base_delay=1.0, jitter=False), 2.0) self.assertEqual(exponential_backoff(2, base_delay=1.0, jitter=False), 4.0) self.assertEqual(exponential_backoff(3, base_delay=1.0, jitter=False), 8.0) - + # Test max delay self.assertEqual( - exponential_backoff(10, base_delay=1.0, max_delay=5.0, jitter=False), - 5.0 + exponential_backoff(10, base_delay=1.0, max_delay=5.0, jitter=False), 5.0 ) - + # Test with jitter (should be within ±25% of base value) for attempt in range(5): delay = exponential_backoff(attempt, base_delay=1.0, jitter=True) - expected = 1.0 * (2 ** attempt) + expected = 1.0 * (2**attempt) self.assertGreaterEqual(delay, expected * 0.75) self.assertLessEqual(delay, expected * 1.25) @@ -132,7 +128,7 @@ def test_should_retry(self): error = OuraServerError("Server error") self.assertFalse(should_retry(error, attempt=3, max_retries=3)) self.assertTrue(should_retry(error, attempt=2, max_retries=3)) - + # Test retryable errors retryable_errors = [ OuraServerError("Server error"), @@ -140,10 +136,10 @@ def test_should_retry(self): OuraTimeoutError("Request timed out"), OuraRateLimitError("Rate limited", retry_after=60), ] - + for error in retryable_errors: self.assertTrue(should_retry(error, attempt=0, max_retries=3)) - + # Test non-retryable errors non_retryable_errors = [ OuraAuthenticationError("Invalid token"), @@ -151,10 +147,10 @@ def test_should_retry(self): OuraNotFoundError("Not found"), OuraClientError("Bad request"), ] - + for error in non_retryable_errors: self.assertFalse(should_retry(error, attempt=0, max_retries=3)) - + # Test rate limit with large retry_after error = OuraRateLimitError("Rate limited", retry_after=600) self.assertFalse(should_retry(error, attempt=0, max_retries=3)) @@ -174,7 +170,7 @@ def test_make_request_success(self, mock_get): mock_response.ok = True mock_response.json.return_value = {"data": "test"} mock_get.return_value = mock_response - + result = self.client._make_request("/test") self.assertEqual(result, {"data": "test"}) mock_get.assert_called_once() @@ -188,10 +184,10 @@ def test_make_request_http_error(self, mock_get): mock_response.reason = "Not Found" mock_response.json.return_value = {"error": "Resource not found"} mock_get.return_value = mock_response - + with self.assertRaises(OuraNotFoundError) as cm: self.client._make_request("/test") - + self.assertEqual(cm.exception.message, "Resource not found") self.assertEqual(cm.exception.status_code, 404) self.assertEqual(cm.exception.endpoint, "/test") @@ -200,10 +196,10 @@ def test_make_request_http_error(self, mock_get): def test_make_request_timeout(self, mock_get): """Test request timeout.""" mock_get.side_effect = requests.exceptions.Timeout("Timeout") - + with self.assertRaises(OuraTimeoutError) as cm: self.client._make_request("/test") - + self.assertIn("timed out", cm.exception.message) self.assertEqual(cm.exception.endpoint, "/test") @@ -211,10 +207,10 @@ def test_make_request_timeout(self, mock_get): def test_make_request_connection_error(self, mock_get): """Test connection error.""" mock_get.side_effect = requests.exceptions.ConnectionError("Connection failed") - + with self.assertRaises(OuraConnectionError) as cm: self.client._make_request("/test") - + self.assertIn("Failed to connect", cm.exception.message) self.assertEqual(cm.exception.endpoint, "/test") @@ -227,20 +223,18 @@ def test_make_request_with_retry_success(self, mock_get): mock_response_error.status_code = 500 mock_response_error.reason = "Internal Server Error" mock_response_error.json.return_value = {} - + mock_response_success = MagicMock() mock_response_success.ok = True mock_response_success.json.return_value = {"data": "success"} - + mock_get.side_effect = [mock_response_error, mock_response_success] - + # Enable retry self.client.retry_config = RetryConfig( - max_retries=3, - base_delay=0.01, # Short delay for testing - jitter=False + max_retries=3, base_delay=0.01, jitter=False # Short delay for testing ) - + result = self.client._make_request("/test") self.assertEqual(result, {"data": "success"}) self.assertEqual(mock_get.call_count, 2) @@ -255,17 +249,15 @@ def test_make_request_with_retry_exhausted(self, mock_sleep, mock_get): mock_response.reason = "Internal Server Error" mock_response.json.return_value = {"error": "Server error"} mock_get.return_value = mock_response - + # Enable retry with limited attempts self.client.retry_config = RetryConfig( - max_retries=2, - base_delay=0.01, - jitter=False + max_retries=2, base_delay=0.01, jitter=False ) - + with self.assertRaises(OuraServerError) as cm: self.client._make_request("/test") - + self.assertEqual(cm.exception.message, "Server error") self.assertEqual(mock_get.call_count, 3) # Initial + 2 retries @@ -279,18 +271,18 @@ def test_make_request_with_rate_limit_retry(self, mock_sleep, mock_get): mock_response_rate_limit.reason = "Too Many Requests" mock_response_rate_limit.headers = {"Retry-After": "2"} mock_response_rate_limit.json.return_value = {"error": "Rate limited"} - + mock_response_success = MagicMock() mock_response_success.ok = True mock_response_success.json.return_value = {"data": "success"} - + mock_get.side_effect = [mock_response_rate_limit, mock_response_success] - + self.client.retry_config = RetryConfig(max_retries=3) - + result = self.client._make_request("/test") self.assertEqual(result, {"data": "success"}) - + # Should have slept for the Retry-After duration mock_sleep.assert_called_once_with(2) @@ -303,12 +295,12 @@ def test_make_request_no_retry_on_client_error(self, mock_get): mock_response.reason = "Bad Request" mock_response.json.return_value = {"error": "Invalid parameters"} mock_get.return_value = mock_response - + self.client.retry_config = RetryConfig(max_retries=3) - + with self.assertRaises(OuraClientError) as cm: self.client._make_request("/test") - + self.assertEqual(cm.exception.message, "Invalid parameters") self.assertEqual(mock_get.call_count, 1) # No retries @@ -319,7 +311,7 @@ def test_make_request_endpoint_normalization(self): mock_response.ok = True mock_response.json.return_value = {} mock_get.return_value = mock_response - + # Test various endpoint formats test_cases = [ ("/test", "https://api.ouraring.com/v2/test"), @@ -327,7 +319,7 @@ def test_make_request_endpoint_normalization(self): ("/v2/test", "https://api.ouraring.com/v2/test"), ("v2/test", "https://api.ouraring.com/v2/test"), ] - + for endpoint, expected_url in test_cases: mock_get.reset_mock() self.client._make_request(endpoint) @@ -337,7 +329,7 @@ def test_make_request_endpoint_normalization(self): def test_retry_config_disabled(self): """Test that retry can be disabled.""" self.client.retry_config = RetryConfig(enabled=False) - + with patch("requests.get") as mock_get: mock_response = MagicMock() mock_response.ok = False @@ -345,10 +337,10 @@ def test_retry_config_disabled(self): mock_response.reason = "Server Error" mock_response.json.return_value = {} mock_get.return_value = mock_response - + with self.assertRaises(OuraServerError): self.client._make_request("/test") - + # Should not retry when disabled self.assertEqual(mock_get.call_count, 1) diff --git a/wednesday_font_vector_with_holes.txt b/wednesday_font_vector_with_holes.txt deleted file mode 100644 index a595077..0000000 --- a/wednesday_font_vector_with_holes.txt +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file From f7a69fcca5ae33baf4143aba23f4ecdbf0f7f8d4 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 22:15:48 +0000 Subject: [PATCH 2/5] Refactor: Implement robust Docker setup and CI integration This commit introduces a comprehensive Docker setup for development and testing, and integrates it into the CI workflow. Key changes: - Added a multi-stage Dockerfile: - Uses python:3.10-slim. - Creates a non-root user for security. - Optimizes dependency installation for build caching. - Copies application code, tests, and configuration. - Sets 'pytest tests/' as the default command. - Updated docker-compose.yml: - Configured for local development with live-reloading volume mounts. - Default command set to run tests. - Modified GitHub Actions workflow (.github/workflows/python-app.yml): - Builds the Docker image. - Runs linters (flake8) and tests (pytest) inside the Docker container, ensuring consistency between CI and local development environments. - Ensured the 'config/' directory is included in the Docker image so flake8 can access its configuration. This improved setup provides a consistent, reproducible, and optimized environment for both local development and CI processes. --- .github/workflows/python-app.yml | 28 +++++-------- Dockerfile | 71 +++++++++++++++++++++++++++++--- docker-compose.yml | 34 +++++++++++++-- 3 files changed, 108 insertions(+), 25 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index a4fa588..d8d0115 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -19,22 +19,16 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up Python 3.10 - uses: actions/setup-python@v3 - with: - python-version: "3.10" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install flake8 pytest pytest-xdist - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi - - name: Lint with flake8 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + run: docker build . --file Dockerfile --tag my-app:latest + + - name: Lint with flake8 (inside Docker) run: | - # stop the build if there are Python syntax errors or undefined names - flake8 src tests --count --select=E9,F63,F7,F82 --show-source --statistics --config=config/.flake8 - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 src tests --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics --config=config/.flake8 - - name: Test with pytest + docker run --rm my-app:latest flake8 src tests --config=config/.flake8 + + - name: Test with pytest (inside Docker) run: | - pytest -n auto -v tests + docker run --rm my-app:latest pytest -n auto -v tests/ diff --git a/Dockerfile b/Dockerfile index de3c9ca..ec2b3f9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,67 @@ -FROM python:3.9-slim +# Base stage for installing dependencies +FROM python:3.10-slim as builder + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# Create a non-root user and group +RUN groupadd -r appuser && useradd -r -g appuser -d /home/appuser -s /sbin/nologin -c "Docker image user" appuser +RUN mkdir /home/appuser && chown appuser:appuser /home/appuser + +# Create working directory WORKDIR /app -COPY requirements.txt . -RUN pip install -r requirements.txt -COPY src/ ./src -CMD ["python", "src/example.py"] + +# Upgrade pip +RUN python -m pip install --upgrade pip + +# Copy requirement files +COPY requirements.txt requirements-dev.txt ./ + +# Install dependencies +# Install production dependencies first, then development dependencies +# This helps with Docker layer caching if only dev dependencies change +RUN pip install --no-cache-dir -r requirements.txt +RUN pip install --no-cache-dir -r requirements-dev.txt + +# Application stage +FROM python:3.10-slim + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# Create a non-root user and group (must match builder stage if copying files with --chown) +RUN groupadd -r appuser && useradd -r -g appuser -d /home/appuser -s /bin/bash -c "Docker image user" appuser +RUN mkdir /home/appuser && chown appuser:appuser /home/appuser + +# Create working directory +WORKDIR /app + +# Copy installed dependencies from builder stage +COPY --from=builder /usr/local/lib/python3.10/site-packages/ /usr/local/lib/python3.10/site-packages/ +COPY --from=builder /app/ /app/ + +# Copy application code and necessary files +# Ensure these paths are correct based on your project structure after reorganization +COPY src/ ./src/ +COPY tests/ ./tests/ +COPY config/ ./config/ +COPY setup.py . +COPY openapi_spec.json . +# Copy other root files if necessary, e.g., README.md, LICENSE, etc. +# COPY README.md . +# COPY LICENSE . + +# Ensure correct ownership of application files +RUN chown -R appuser:appuser /app /home/appuser + +# Switch to non-root user +USER appuser + +# Expose port if your application is a web service (e.g., example.py runs a Flask app) +# EXPOSE 5000 + +# Default command to run tests +# This makes the container runnable and immediately useful for a developer +CMD ["pytest", "tests/"] diff --git a/docker-compose.yml b/docker-compose.yml index c72544d..396a7b6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,36 @@ version: "3.8" + services: app: - build: . - ports: - - "5000:5000" + build: + context: . + dockerfile: Dockerfile + container_name: oura_api_dev + # Mounts for live code reloading during development volumes: - ./src:/app/src + - ./tests:/app/tests + - ./openapi_spec.json:/app/openapi_spec.json + - ./setup.py:/app/setup.py + # If example.py or other root scripts are needed and are modified during dev: + # - ./example.py:/app/example.py + + # Keep the container running for development, e.g., for exec-ing into it + # Overrides the Dockerfile's CMD + # command: tail -f /dev/null + + # Or, to run tests by default when doing `docker-compose up`: + command: pytest tests/ + + # If your example.py runs a web service on port 5000: + # ports: + # - "5000:5000" + + environment: + # Add any environment variables needed for development or testing + # - MY_VARIABLE=my_value + - PYTHONUNBUFFERED=1 # Ensures print statements appear without delay + + # Standard useful settings + stdin_open: true # docker run -i + tty: true # docker run -t From f00c4392b9586c8b79f94e8fdb98921be0280e9e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Sep 2025 05:35:48 +0000 Subject: [PATCH 3/5] Initial plan From 5baccf7b5817b3dcd5ef2ffd6c24b1f54d98431e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Sep 2025 05:51:22 +0000 Subject: [PATCH 4/5] Initial analysis: Identified 3 unmerged branches to evaluate Co-authored-by: godely <3101049+godely@users.noreply.github.com> --- .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 726 bytes .../__pycache__/exceptions.cpython-312.pyc | Bin 0 -> 7136 bytes .../api/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 207 bytes .../api/__pycache__/base.cpython-312.pyc | Bin 0 -> 477 bytes .../api/__pycache__/client.cpython-312.pyc | Bin 0 -> 8034 bytes .../__pycache__/daily_activity.cpython-312.pyc | Bin 0 -> 2242 bytes .../daily_cardiovascular_age.cpython-312.pyc | Bin 0 -> 2459 bytes .../daily_readiness.cpython-312.pyc | Bin 0 -> 2263 bytes .../daily_resilience.cpython-312.pyc | Bin 0 -> 2282 bytes .../__pycache__/daily_sleep.cpython-312.pyc | Bin 0 -> 2185 bytes .../api/__pycache__/daily_spo2.cpython-312.pyc | Bin 0 -> 2168 bytes .../__pycache__/daily_stress.cpython-312.pyc | Bin 0 -> 2205 bytes .../__pycache__/enhanced_tag.cpython-312.pyc | Bin 0 -> 2205 bytes .../api/__pycache__/heartrate.cpython-312.pyc | Bin 0 -> 1955 bytes .../api/__pycache__/personal.cpython-312.pyc | Bin 0 -> 1614 bytes .../rest_mode_period.cpython-312.pyc | Bin 0 -> 2275 bytes .../ring_configuration.cpython-312.pyc | Bin 0 -> 2703 bytes .../api/__pycache__/session.cpython-312.pyc | Bin 0 -> 2114 bytes .../api/__pycache__/sleep.cpython-312.pyc | Bin 0 -> 2080 bytes .../api/__pycache__/sleep_time.cpython-312.pyc | Bin 0 -> 2445 bytes .../api/__pycache__/tag.cpython-312.pyc | Bin 0 -> 2038 bytes .../api/__pycache__/vo2_max.cpython-312.pyc | Bin 0 -> 2107 bytes .../api/__pycache__/webhook.cpython-312.pyc | Bin 0 -> 5441 bytes .../api/__pycache__/workout.cpython-312.pyc | Bin 0 -> 2114 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 213 bytes .../__pycache__/daily_activity.cpython-312.pyc | Bin 0 -> 4084 bytes .../daily_cardiovascular_age.cpython-312.pyc | Bin 0 -> 1343 bytes .../daily_readiness.cpython-312.pyc | Bin 0 -> 2348 bytes .../daily_resilience.cpython-312.pyc | Bin 0 -> 1625 bytes .../__pycache__/daily_sleep.cpython-312.pyc | Bin 0 -> 3846 bytes .../__pycache__/daily_spo2.cpython-312.pyc | Bin 0 -> 1426 bytes .../__pycache__/daily_stress.cpython-312.pyc | Bin 0 -> 1493 bytes .../__pycache__/enhanced_tag.cpython-312.pyc | Bin 0 -> 1269 bytes .../__pycache__/heartrate.cpython-312.pyc | Bin 0 -> 2243 bytes .../__pycache__/personal.cpython-312.pyc | Bin 0 -> 1498 bytes .../rest_mode_period.cpython-312.pyc | Bin 0 -> 1567 bytes .../ring_configuration.cpython-312.pyc | Bin 0 -> 1581 bytes .../models/__pycache__/session.cpython-312.pyc | Bin 0 -> 2330 bytes .../models/__pycache__/sleep.cpython-312.pyc | Bin 0 -> 4950 bytes .../__pycache__/sleep_time.cpython-312.pyc | Bin 0 -> 2116 bytes .../models/__pycache__/tag.cpython-312.pyc | Bin 0 -> 895 bytes .../models/__pycache__/vo2_max.cpython-312.pyc | Bin 0 -> 976 bytes .../models/__pycache__/webhook.cpython-312.pyc | Bin 0 -> 2952 bytes .../models/__pycache__/workout.cpython-312.pyc | Bin 0 -> 1493 bytes .../utils/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 496 bytes .../__pycache__/pagination.cpython-312.pyc | Bin 0 -> 1948 bytes .../__pycache__/query_params.cpython-312.pyc | Bin 0 -> 2135 bytes .../utils/__pycache__/retry.cpython-312.pyc | Bin 0 -> 4881 bytes tests/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 194 bytes .../test_client.cpython-312-pytest-8.4.2.pyc | Bin 0 -> 110535 bytes ...error_handling.cpython-312-pytest-8.4.2.pyc | Bin 0 -> 18392 bytes ...est_pagination.cpython-312-pytest-8.4.2.pyc | Bin 0 -> 10752 bytes 52 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 oura_api_client/__pycache__/__init__.cpython-312.pyc create mode 100644 oura_api_client/__pycache__/exceptions.cpython-312.pyc create mode 100644 oura_api_client/api/__pycache__/__init__.cpython-312.pyc create mode 100644 oura_api_client/api/__pycache__/base.cpython-312.pyc create mode 100644 oura_api_client/api/__pycache__/client.cpython-312.pyc create mode 100644 oura_api_client/api/__pycache__/daily_activity.cpython-312.pyc create mode 100644 oura_api_client/api/__pycache__/daily_cardiovascular_age.cpython-312.pyc create mode 100644 oura_api_client/api/__pycache__/daily_readiness.cpython-312.pyc create mode 100644 oura_api_client/api/__pycache__/daily_resilience.cpython-312.pyc create mode 100644 oura_api_client/api/__pycache__/daily_sleep.cpython-312.pyc create mode 100644 oura_api_client/api/__pycache__/daily_spo2.cpython-312.pyc create mode 100644 oura_api_client/api/__pycache__/daily_stress.cpython-312.pyc create mode 100644 oura_api_client/api/__pycache__/enhanced_tag.cpython-312.pyc create mode 100644 oura_api_client/api/__pycache__/heartrate.cpython-312.pyc create mode 100644 oura_api_client/api/__pycache__/personal.cpython-312.pyc create mode 100644 oura_api_client/api/__pycache__/rest_mode_period.cpython-312.pyc create mode 100644 oura_api_client/api/__pycache__/ring_configuration.cpython-312.pyc create mode 100644 oura_api_client/api/__pycache__/session.cpython-312.pyc create mode 100644 oura_api_client/api/__pycache__/sleep.cpython-312.pyc create mode 100644 oura_api_client/api/__pycache__/sleep_time.cpython-312.pyc create mode 100644 oura_api_client/api/__pycache__/tag.cpython-312.pyc create mode 100644 oura_api_client/api/__pycache__/vo2_max.cpython-312.pyc create mode 100644 oura_api_client/api/__pycache__/webhook.cpython-312.pyc create mode 100644 oura_api_client/api/__pycache__/workout.cpython-312.pyc create mode 100644 oura_api_client/models/__pycache__/__init__.cpython-312.pyc create mode 100644 oura_api_client/models/__pycache__/daily_activity.cpython-312.pyc create mode 100644 oura_api_client/models/__pycache__/daily_cardiovascular_age.cpython-312.pyc create mode 100644 oura_api_client/models/__pycache__/daily_readiness.cpython-312.pyc create mode 100644 oura_api_client/models/__pycache__/daily_resilience.cpython-312.pyc create mode 100644 oura_api_client/models/__pycache__/daily_sleep.cpython-312.pyc create mode 100644 oura_api_client/models/__pycache__/daily_spo2.cpython-312.pyc create mode 100644 oura_api_client/models/__pycache__/daily_stress.cpython-312.pyc create mode 100644 oura_api_client/models/__pycache__/enhanced_tag.cpython-312.pyc create mode 100644 oura_api_client/models/__pycache__/heartrate.cpython-312.pyc create mode 100644 oura_api_client/models/__pycache__/personal.cpython-312.pyc create mode 100644 oura_api_client/models/__pycache__/rest_mode_period.cpython-312.pyc create mode 100644 oura_api_client/models/__pycache__/ring_configuration.cpython-312.pyc create mode 100644 oura_api_client/models/__pycache__/session.cpython-312.pyc create mode 100644 oura_api_client/models/__pycache__/sleep.cpython-312.pyc create mode 100644 oura_api_client/models/__pycache__/sleep_time.cpython-312.pyc create mode 100644 oura_api_client/models/__pycache__/tag.cpython-312.pyc create mode 100644 oura_api_client/models/__pycache__/vo2_max.cpython-312.pyc create mode 100644 oura_api_client/models/__pycache__/webhook.cpython-312.pyc create mode 100644 oura_api_client/models/__pycache__/workout.cpython-312.pyc create mode 100644 oura_api_client/utils/__pycache__/__init__.cpython-312.pyc create mode 100644 oura_api_client/utils/__pycache__/pagination.cpython-312.pyc create mode 100644 oura_api_client/utils/__pycache__/query_params.cpython-312.pyc create mode 100644 oura_api_client/utils/__pycache__/retry.cpython-312.pyc create mode 100644 tests/__pycache__/__init__.cpython-312.pyc create mode 100644 tests/__pycache__/test_client.cpython-312-pytest-8.4.2.pyc create mode 100644 tests/__pycache__/test_error_handling.cpython-312-pytest-8.4.2.pyc create mode 100644 tests/__pycache__/test_pagination.cpython-312-pytest-8.4.2.pyc diff --git a/oura_api_client/__pycache__/__init__.cpython-312.pyc b/oura_api_client/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b026440f2bfeaac1999934af88b04bf96f136fb2 GIT binary patch literal 726 zcmb7BL2uJA6t>f(Nt3qQK--Q)+#*CfjxYq8CXkTOmmttO3zM2EKG7z&a}hVvi7aNQ zAN`=fI(P;c{W+G__I}ZDQv#p^aRv@mt6rY5P!@Mb=8M zAT>#?cr2B)Ew_Q(Q(;bpStD8#EpZhUhq@@l^k3IaB%etoblDaH`_V*LQ-ShC&Q{*u z=+s`UnGnqhzc0InVTz^Eylyax`rowJBGJ;`LN4 z(9QC{O4nX#Wxi}W)Dh<}^b(3d3+R>RIp-hA8FVKX`{TJ*Vr)tn+l*gn^Kz_V;zhN- zaqz`kUt`W?AuZ>lMRgI}RXQtkanI~RE&SZ&4S-)AO6eDJ|2w&U?u67iCjfsM*dg@V S*Wvb?=(pRCoVUK&2K`^p4bG7O literal 0 HcmV?d00001 diff --git a/oura_api_client/__pycache__/exceptions.cpython-312.pyc b/oura_api_client/__pycache__/exceptions.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c350aeb424347319b0b35f5a608208cb365c93ba GIT binary patch literal 7136 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 Nl0fBZ2vN2*{{j&Rbq4?d literal 0 HcmV?d00001 diff --git a/oura_api_client/api/__pycache__/__init__.cpython-312.pyc b/oura_api_client/api/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8b95c06dc3bac87f10e784c1e0ae50243b881281 GIT binary patch literal 207 zcmX@j%ge<81Xgd(WT*q_#~=<2FhUuhIe?7m3@Hpz43&(UOjQbw0iFuUIhm<>B?`Iu zDWy57#R_TpMGF3C%v LZ!xG9u>d&$wbnUn literal 0 HcmV?d00001 diff --git a/oura_api_client/api/__pycache__/base.cpython-312.pyc b/oura_api_client/api/__pycache__/base.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66a28cd594561ff0b56d8b9cf12d3e4ebb4b89fc GIT binary patch literal 477 zcmYjNF;2rk5ZpUEU~#qa?^8=39uj znIF}Pw#fSgng{5027YG+{JT=#nn5W=t5bVerq|ZCHhX`gOY68kTTC MZz)CR3ka&f4@^F1%K!iX literal 0 HcmV?d00001 diff --git a/oura_api_client/api/__pycache__/client.cpython-312.pyc b/oura_api_client/api/__pycache__/client.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e5fd08af6e5537a6f6adb76f63e0bdf933381b4f GIT binary patch literal 8034 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 literal 0 HcmV?d00001 diff --git a/oura_api_client/api/__pycache__/daily_activity.cpython-312.pyc b/oura_api_client/api/__pycache__/daily_activity.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c8401706e954d2693649dc1ffea2665ea9d49e65 GIT binary patch literal 2242 zcmbVN&2Jk;6rcUfdL1{^acDx)b``aNOJbK6kZ=&9s8yj-gOs#A7{O}e9VeUYhnZbN z9VJo^IrN0+DUlG84;+HRp-27)Din(Y(MnM(apHhvEQ*mW*ped` z<;WK80Y@n+PNJBI=8~;C$zqZtJh@3sd4re(RNfKd`-_7$C7bvICs{sJKB)wj=Nfh> z-F3mpa-nD%0R^poX!vx_s|J*TAvOLo=I&unqH_{ zwyA$nrL3k`3}!ffDgJsa6YWWB0gO#TNs%*2kxvl(GZQ9H1u9aB%4L2GJn-KX{$)*s zH7VMa=s;P4RHdQ<$x5a~6J^mWgzWRQr$5g%3~{eugZ){tyk$r#nR)i=D|D?W3EyQCl)F46l&cKUGLU<^->>P zxs!Y`L;nL156HnJNnfbndaV3(>DSXY9?E;eXEtXZ&HOez)fk@I8=QK4uPKnsbc^sA z^`UYglb0?whA!9dgVoJt@A;*VN4#Hfp0g1u=-1vq1XwA<^vsUdW;b5mgu3}@s8+8qo93!UGvIOCv=v3 z&c2wg(fjv*xO)C%uI5=9fXWJN7!=E(P~wFVv|mB-G6)}01^Tfwpu_PK*sz-Y&&1L2 z;wvB?kme}KjP@|))b#es_Gi0;GcBRZl*z`>bp0a`yUH}fw>c}!&dz3&p`z=q;ZR)< zRb6*HvudL~sKd{MVaGcR<7fEdh63~*!}3Sn$41e@oY@2lgd@5<%9JH>iUK`+2gC+x z%H+yBd-F(SSBEHklnw|;e7tiRhhJKf0Sni8yn*YNn(+x6kPSs!bRztKof zH3x8AAtPtE<{#Z|;@PH3)bp(*TDc)9w>N&SDS&>K7(b9e$C2RoXS1nL4r&#kV^1y$ zzOjMj&?upB6rX{f#?L|H=N90{mf`CWUOc)DSW(;0^}hizESPUNa`Zqouxvki(PCu! y(R!}D7o-0^dlimjv-ui`roeIBQ!=?v#`a16Dfx7tymcVI#^t`b^)~?(W%&;xVpG)s literal 0 HcmV?d00001 diff --git a/oura_api_client/api/__pycache__/daily_cardiovascular_age.cpython-312.pyc b/oura_api_client/api/__pycache__/daily_cardiovascular_age.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5aa87f4342295f5712d66bb8bb7a467ac0ea2771 GIT binary patch literal 2459 zcmb_e-Af!-6uE%)1x3om3se#JqvNe!j;87S+-A{OTmm(w|y*Le=dyd$`xF2=gxNUp5Q zu`W7YuA(ckE;$KT)m1mCCpp5CyTp>$h}A{qEuntD-d$GGi5ECX^O17D7}~yPI+66y z10~HxqGg5@di4V{pfi3cqzp#Ab0yocjL%Dym5rjwOgDhzm+qLhQ~uComhC?>gM7&` z+3fUccvC)XCTFsmnxTP@4l~olv547SZ_x!9Ocu@l>E9m8d+=55W%n zSwe?75$2?LRia(<3OFz63b-#>5>3pDma-=5YMD!`(LiU@rXw{7O%@u6LZnd7s%xr8 zzYLAgU!q&5*hy7wR`iD~x z{}u6jeyg#2RSLVBijrdbd29Ed`Y2 zeaE4Bv@IK>YaBgj0J^{uxz3F5ASw)_70G$W2E3yl!!?(v!6*ddol z7_c4`r%;>)5h<)ep#aJRK|?j|<^Ak}@6s$Qc^+liFMPI?^-IhwmfJhSEZRo>)hwv5 zv^G;LM^_3I6CIMJgF8kmS(e}JjCbZ_$I-vweGrey!3gOc+5C80*?IG)vr|vxy}onn zH=o}8xo@n}H@4S3zVoOmkkqvr;Zy1pWs%Y^hvnUH0@fNxr0ebBKK3M}w2(U1Ch z6aydv3z}CbY8eYUT9! zvrjCo31P zRZ^L%1hcTKZ+PRK&Av@*bEqq>%+m&Oi7gIIyuYEA}fPG%1Nb6{AwqWu?LT>e;}5#i+{A|d2Z1j3_;1ENph*A# literal 0 HcmV?d00001 diff --git a/oura_api_client/api/__pycache__/daily_readiness.cpython-312.pyc b/oura_api_client/api/__pycache__/daily_readiness.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b38803de2fe98c134c8fe9f6c110fda1db0c6bc2 GIT binary patch literal 2263 zcmbVNOK%%h6u$G!crhW1&9b9tSQZO#!~Q5R#I zcZOU^mttFRQm(AaZdy+>h())NCGH|?2#beYa=xCaNcq%nw3BBe>4Q>e`=04U!aWa+ zJQMMj8Dh}#yJmn_{Bnp1^v2f8wqqHelrgCoC6kzL0Nc;sFm0!@f=$cza1gBEpyYc2 zjEL zL+we6vr$#}hWUb7MS0%T;4)chwc9vUEQ-ED+VCruUnslS3jBz&RmSiat@o4Xl(fv3*PJ+tP+-NxA7+&tBfea7npKp^OhNnd`P^Qo8lHw-Q4< z;9>t$wOO^Kv@NCJd!cE2wzqy312Z(c&Tqe{l>zI-i9=z+k(RuLg`F_mQr>>^X z2XBM8k4|UN*rnS0kEHKk`RUxv2jX$|!rrxq*M82<*R%7-nT1Ei28VJBO~mHp2hypC zo|~(WUOA9J97$K2a0ZIfZ8r?tD)f@RlPq^(*&c$Fpz8dZ>3cGkW4!Qr^3v_{BIQ+QU72{{?O1 zqW8Zzt2~3V8W{ty@*K&6A~`B(J%xdBYCliKc@P0b7HCK=fR3i0Mv3jq|A9Xj>Dh}Q z?xV&8%1!hTW<)*s{J=fRTyJt6!d$G6s0-l(WW#WM ztL#uaW58F&bdniDLm;#OM;xRep)80KkxWpFhE1lapoqkW$H9^`?NUMifeT_6HAJ-d z#<6zw_|nUNq{fBC$GPeKd$p?veC^Hp%tAeADseMSEp=4Tj;CH|aG)QjrcVXXi6`{#<@|6YhLsYqvL_aaR@~5b zaU4)IK`TN}4N##5sB7@~CA5%43s_L(>n}6X|*a u|661ZE~H5p%z|ie48#0_W>3)M3DQo`Yk#pE!=7edVze*yKM|-n(7ypbO842M$5ukR$&C9BNUCXr+isoRByeIS8pIX4ao|qZW9UXXbs+{N8V7 ze;gcC3AEfFKW$!@3Hc2>?IyL1#wTFx6N^}!LtHM;xqP1Iu+KY!E9S-6797cy^Kxv9 z&VZ}rmDrY?gsbLNHO*#`=04U(j5S%(8B);dbu*wVekr63dc$ia+p&yKOO%z3qRC7*fbC;9Oxr21(7<+V>J=zhMc)f( zlI<`Kt(E1d7Ynt75KM=bH+p%lj2#4 z4y-E>u9R0GV9AnbVqLV9Ju$DAxr`bO_4%|MsX=J6(7=Erg?d)gQa$=bXoUVY^&*+k zu*AHA*zy(?J`{r4a6s&nkOLLB3RmGPLL2B^u|l@^_FPwQwI8}W1W#QNzvquyt9PYv zpgm~`HmOM8a$j<*BqN#&aG9#L+7b;l3m-sZofpt7zff|i7Y13iv+x795#%~O+}#a4 zr>!Cga8O9Kb)RYBCe@0R*}j#1VoMvJoOaW*p1-^cVotk_SjLAJ%?;Z#k#armt;87j zdD!<@ZD!@Pwxtz(FEnk>_BPHz!3@o=`#YLm3Mec1jzbH0xgNu2bYTOA%S~2)2QV{) zKavYb>M$BKTyvYkVjzDIG6m)`awG=SS!XHeut5|TP+SBNDR69@1p%ZuX};CtC4JL( zsm@BCN16V)&$e~H#LQy3R~cr}Hk!BUU_Rw#wpflXZ%||`yn2uOM(5RcvVFj1pS>Of z2;K*Amz+(L;hE}3$I1_{{e1D}J^5s0Z2#K*Yrl-l)kfw{QrY9r>Htous)9%k~MR2CnaGTdf+^I}EA8eNDdMk6LNA z%ZHr=vUo@izLgxp&chC0F*8gp0BSh&9CEZ$q=$k}XGl*VIqkz6n!g@rMtitt@4umM zT>t(bcdtB;yE+>Nz%l|G0mafNu(-m&DB3Tfco9T^=mH(t80cu~nY`GU|4%e}8INB8 zahKG`NqW48Hy8Bd>7NK8P=3&w12u3+D_0cTeJOhfFpH+Y=6yYf)?WyWAh|lkCk_t v_`kaV_`qu8eNHyn5A${{ zaO6lm9?7kCc)ye&9lL5ys{mK?bt zN497WI7&f@Y{^bIYC(09g(OFKa+{d)4lxI)d?3Wz7Y3_JCh;qFGJL4qEeDq88g?k% zcfrVTp=cTb1+CsNe7fva0?ME_yjrnrQ~#twSye9^%y4{IK77-#?COe5X?dCYWzY3# zIMnmq@l0wL<8xz~Xm?txVC)h~3Y*ixaK3|n&}lQ4s`=Rt9IwS%hvpSw}%URIp?(% z%pdj$s8;fr7OYdPOqu1G*)vNz(B!pSuKE1wJ^b?8J%lmZylkvlu7ULG3vW4s_gsX% z5_MRV*E*I~^xVL(T+3ZMo4XMhy`Z-yauuJlqG#K*h)3okob(7MpsyTL_8WkdA=IH< zv@Jj`OzDoXNp(hn*?zzj7|Y0^=u^AI2BE`JC@!G52qIKio9Dg^-MB7KZ{*fJhvrzt zbt%hz>aoq7S7AoE+OKq@Z0Yf-IWYSJ%$BR+_!>oGLUH4u^=@&uW_tK!pE(PI_1^*U zfSgW|;hFk}hsxvGCl_x$l#fR)?JhoA{BdNyF*1KVm^r-K6i9lZMfkM(P&t*!3sa4u zh5Gv-judc13zMDWb;~UFle&8u?!mNO0+h%3=h{a5vFUdM<+(4(SNuWy`fT$-_hOhl zke+XoUfBAsOGae8q4`jJo1QIyoBDm!`aARca>#2R+|;~M#Q2W8Z|!qv89ikGo0F6O z!pR&P25>S08v(`ADDZ6r$0*vDQM?GkM=Nj3>{;dCn*I+Qna2IDfp|cgX_6l6 zBgKW;{l)!-qro>?LYEXTHHK#EV<3){S%z!?s#U!Mte|) zuY+O7BMif4_=tuATq}l^jxHA)Lkm-7lPC~{X!9sRmc%X!{QH|Ac1Tku*Iqx)T|b_A z<@dy>l>Iq9xp%*QeP67<*|@UMNN1Z8cwyGa#NKQ5k-AwQZ%oWJhUc3D=vT<-mA#cm zcbd4isSlI9NIYZ6e8@1CNH z&cdzEuEIx?;nNU~o*%9RmhH!{&(!%bK#|&hw(tJOXhlF_MCX8ds=O5g|2De{qj;tK mDG*J8Yu#8@u!0?b8}x@{)2#ulKcx=-8pLj literal 0 HcmV?d00001 diff --git a/oura_api_client/api/__pycache__/daily_spo2.cpython-312.pyc b/oura_api_client/api/__pycache__/daily_spo2.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eb3121c3fc0f7c2f9d7953324b7c866dec2f8d01 GIT binary patch literal 2168 zcmb7FOK2Ns5dQb+)yj|h729zf)^2FCjxB-Gr&3B&5=;wmz)mhYbXjYE+iUMb{dYwx zV?z!;Q~EF}XF7sb;6K0>&Ppq`+CEz^4fQtdzx5fr?b3@+v<8EAVd#|1u`Rm=w=Sv}aX; z5T$|wK}wcHQ>&t-?1}}o!s%+1YI@`679%wXO%@s$UZhaZN-Wi*pNB^1Z&ELk84b(K zD~e4&QDIsTx`qQ{kAxhkxNURW{I<}7bw}JL8+>c7qc>VO;x5e-H!{+K8DeP%Y zT7pfsrEj<|xMiY?rUsX(TC*+CP_qzj4e{12zgTvu7X}%%z2r7q3-awA?(2k{*OoDS zI3lFls?W4=ooXe@Y~RY9+0sHLuif>mmoM+Ym)9O3it*tkbItZlq*j-EOEJ3VJZ#gY z#h|>_vb3V_g{JM<-rAYS&Cu+)ymdKS4k#=7jzf!hVKxTI06OM?w{pVOZvjq*Fh_FH zu>rQI&v4C6YA_0v4nn5DoGwRVK%G_A3mw*nq94T|h)7{gh6hl%B+1E(+u3#BrCCk{R5c1kbcQk)BW_oM5Ywv&4F@C-N zFBc~-=3HxyCCyh(=R2*t9V!r#3NE4 zCnMusZ0OeyZX8S<^}gK@+HAO58_=r?h$BU3__pf8;^Ly7jugZ2OqUu)q#B0nTV)6B zUIV@brjyJt^qJwS83|BJ3{xCe6dOki<78J*AO!K@ada$=T@?5aToAjYE|c6_$Jv|5 z*RKDO8kOdsk6hV*SiN~5R^P2n&DTaUbqPjc)$rv0o7Lf}Rh_6!-lz@D)q8MUA){0K z%TMmrac^BE>SQC0R%Sq&J)XQ;7eGHxjh#xM6HoB&MZGVQ!%7J#*Aa7CnS49-uR0bIR3Qvb#C@c^-lsSj`43%*elZj literal 0 HcmV?d00001 diff --git a/oura_api_client/api/__pycache__/daily_stress.cpython-312.pyc b/oura_api_client/api/__pycache__/daily_stress.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..23698f2d36078449a23c7c1b6d222959ea09f0f5 GIT binary patch literal 2205 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 literal 0 HcmV?d00001 diff --git a/oura_api_client/api/__pycache__/personal.cpython-312.pyc b/oura_api_client/api/__pycache__/personal.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..edc85285e752699bd5b4c6919b4b11f20bc228e9 GIT binary patch literal 1614 zcmaJ>OK%)S5bk;I?0TI|vJjgDi&hb+4i0S=oe$O~AM&(8XR#2l)tyQ`|JzWQo@ znw_m7__}}o-2YKS=y$OxKYgO?Uj}6h`N%=O;;W$&sg4?Hjuz>Tt{`9Y_0Wh+$5fDt zt|8ypK)(4%Exet|uwq-I_6^E-61yP|;$FfcHw%&&)7VdwAkJ_Qr6G-IoXKWHtzK^C}SFrs)p$okmDjP@v_u}5*1;H*z^ z8 zAR#W&#rWm>oqiJ04jaTVWu5he-RmR}E*&0Th?@qa2+{$ylTNzbPKUWgNC0IKGCEr# zHwCj@-pLn*_!{lj(cHqO{_V^@Y*AikG#jv(M2Szs+$?rSa21$JT-QJWT#;Che`~_x z*yk&cl;Y@1b*!*gGov(EGfVAY9jm4FO*B?-BIwuNHJahF)+2NPj>~Sj1L#?t1)e0w zfK&FrO*>pGUiu27zrJzUU~E~ABxsKQIaka}W>r@e+Q_Yzx|yxL<)L`0Uh#_DjwPKslTQJy9-r(CoMn9F9x@u^jskrG=Od$d?4dcdFm z7&XC{zYPM2bOyaLzjf)mOHZ1uon~wMt&bl!KiRZ?o0a ztTDIQU=47CakypI1!-e9qJ-o%BB(bAgGp7CuDuBDz1bOKP>(phz;~qe|JwejfLIQ=={=O+DT*nm)G&GSf5o- zY-SIxK5Rbt=$o$|j<*{ZcB-wv4Y(;nia^(Gi=BlvH?v_H#H%uoA_b9F-jHnJ?Gn@? zF`35#+#lrj(z;xfe1&1S5HrHhf!Ng)MR|(ed5#u$(BdEH&40{u%KRPzA)WsPQR%6( literal 0 HcmV?d00001 diff --git a/oura_api_client/api/__pycache__/rest_mode_period.cpython-312.pyc b/oura_api_client/api/__pycache__/rest_mode_period.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..60577a53959fdf3b0d902ddff230442cbfd2f059 GIT binary patch literal 2275 zcmbVN&2Jl35P$ob^*SFkcG`p_)sj{%hQzC+fK(R{AgTzY29&fC+5@YN_a$+%ALi{E zNR&uD@*g^*dde{oJ|ZqMet{*Y@P~Kq!N{v_z_rve_J?bOoTBho|S0N zk^;d>h63SAwnS4)qOEL;hPuw>)TqDPrMXBALW_kaMjI*AvlC18=$D}x`fJpSWJbdZ z^NM2ASycE+h-AY7u|q-*RNOYXO@32op}Zw-l2yJncdA!guY5WL@4PAg!0$I#Z%JWK zYtj}y%GQ(!J9M`A#oCDsca)`y}W#RU+N0*A-G29Vw)d8#L~`ik#T zomD)KGW`pmt?7P+S>^RkWm@HuncS*_`IMLW@_KZ6nTFs?_oJKbN8gwNd^*xG|Lo=H zfA9{7`{ZPT3|_5%c%c0F%A*UnAIL|UOFOq7-ufjoS<6fw_2v&guL~qQ)gXLUeW0Ak z|!VN+bMGwmhBKo3|xPvtv1V3=P)D&_YL`$-)|<_ z79Y0L$L1k5_-1Md8;{y-#k{bz0GQ#>bGXq=kPZSqogW>66toZC*Zie8FIvMLd;bM_ zAkJP-nSY;n>ANz1NrJFAP$v0!?K+d=H}*d=}0k6&vL10MyiPw zt>U2FYr+S|a*`Q_K`^YvkpQX4kP2}^vSG9^a5jblF^Lb4!)0mgqQHOPg4iZ?nasR# zq~AEY`tqNtA!+7uc5L@v^~Ro9y;&Qds%7(a2}WU6W_0)UYNl#eM{1+5)&?i*JvgqA zq4C{?hj;3@x2{4_Zlux54@mma=;gWq`f+OPL;{_7f_Km5`XV`8F9S6@Vo|X6h9#HA z0Y$@D89Hh~9%?~;5x&F>3raL}cDfRloFJ)L*U!%3m%$10XA4FGT8vkWO*dXols6On t-)7g~Mohh60z_TlIPO<6aZE;ziGEC8`y6VikG2UVbktU*OIw?y4}pt{3azRl)kR5r@qyLGJ5ILUhnXEy z9Jx{tIrN0oQz{{(95@7(Ly!CqaA?GVuu?@OxFB&bN)S>dB))IgcAN(kI+nlr=KVc> z^PBy4V89?yiobm~|4WvTpRv&?T3cEF2$Ut_5J&QeFO?-$3 zCec-|&)3U(qHA8(H_C?JU+$NPOs)_ovq+pimU*P4`owtqf$hcG z%>WcridDxJ3~J*&J7UveT`&%<;n}+DIo8K@#v4}6=C&Wf^6_amn0r4As_tB!+qly- zi)vvIv3UQEp=+VTyh>_CDj@_(S)YY!i7--@98#9E1b)^EJXe?V*9eq(P8x6@c;-MW@m0}|jHWwDU9qTM`Qq2h1r@|89 zZ5G5C&P1ID6*aLo6__pz>AC>1M1+)f6VjBLa#QJq`>xs~x8%;)W__!Z?#(VpcvIza zQw=0X{#tp|UUgR!eVs{;>XN4RrSzFJLrl$HgeeAXmn%$A4(Xtk-h5CetkiuLh$!Ek zSqcR!((8~hdhx@n^pmOXq$Gv38ghzpqFXXcMWg0cY|m>@*ROdli>OT_nC7wW&Yi<3 z>|j&RjRXxsL94C@D6(fgMrRuo1D%{P#{%f}wi?`H=Ovt&V2#0YIF2*w1{JUFFo(`F zXrZN%;PncmC*3H5Gg(jp8>>{zv&6wWI8}r+s@G~E7t|Gbqiga-J{J|cEsW(R9Ex-X zp|NRk>IjDBIPBlv4~zk?Ad=>0h;g3|l#29n;JoA>n;ebZ%{tb~H?iu-yQC-3?W?us6M3I9#Ylj8{U>V-;k90xq$(0I>ull1Yi< zD`Kh~F&$?rfTKXf1D0>!W)^3_FOW|i#>`BtM$D`795nax`5#73u8y2s%f0#I#E}Q*Rz_PI z89lX5lu_fpzM+%7=IY>?mA61V(a)^IMkMaMVG1^8R$oD(~q+7R~Kc-JsmRG6)tTO`qz8c<4TcUy+p(G26wvY z@umd95Qo>(0|*&4w9Jve{n!L?ShdM`1KtPf%Tg+Xm< z?ci%I1?p$nLmL{@sV#VV$sC9?_}m3^J>KZ}a0%CEiA(Vql1mSrmi9jY literal 0 HcmV?d00001 diff --git a/oura_api_client/api/__pycache__/session.cpython-312.pyc b/oura_api_client/api/__pycache__/session.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..561af1c419b5064a30dde41508eaa85886a7890d GIT binary patch literal 2114 zcmb7F&2Jl35P$obwPPnVc8Eh0SV2kRG_|F)Ad#X9P^$t`gOs!!_P}c6eNLS0hk3ge zI5<)dIpl=YQz9WGA2<|+Lyr~z0vuxXK($iUNQeUmBqN7QPt5GDKN><28)atRysw$x z{O0+mbUI1k`}B{WH?GTs{ECC_C&fnRGcfjvNleZnHm7kmukjp?c}uWGO$=?pl5AO% zLtC_lY(-P-gq8@$l9jZFwPB9%M^~xm!Gaa)Rb9uUK`M6Ka!qQ%^0BpA#WMBJYn0XX zs=*A~D@8X(G7%3tYhdgXN;J+S8lNEeXC_RZ3RI*LmCO7jtiXR$IKwA`PYP!xI#gC5 zJxNm_LCKV8qAZ%quBavJTs|2L#StzCNzXT!uVZ3?LLD=*k`DdS*L`=3I)TinUt>;5 zjH5(_1wqg{7KnY~bD+Yn!8Q1X&;@iyY>-X9JJ;8n-GcUAP|}9@1AiE=-jV#F?xZPH zNJILL`;W4+=S$f$wvRzhe`MRg$Ox@~Mx zolzjD=QG4TF9)JWtujl&fTdB4pg0F2P*_ZK5Biiug?M(YxZ&Eg$ZC#5S@8>(Z57=b zGphALr5n|X9^G05vm0EYS`Wt8sjtVu>b**CPsd;s&YZ>QP`nJnL+F4=YyxzUJ*(E+Q!iq|8+gbfh`XenCE46S=|^UoUp4Kc)Rm6V zEB#b!boO8a#E~+~@WIRr%gf98;Xu)K$FQlc2T5JGU9)DPozmg$HmqodVXO=v!$5#; zVOYz|_OT!>rga4pCt5d0QZMNn0k1Zyy&gA77aJGd&?)JkCxX+-Y9E zFE-z4O-@#m*g`vj{uCLXK3KhXtBrfxBP2Q58AWR` zEzLa;+62_Y)CsC64!nIipAKZdUIlgyWTN03=vQnSrW53_bqAWY0L@xhgWo)c9Ur`U zdK!?TmRE?|H$nq}2*yvZtoaqo3!k3|kmo^qro0=0evQq*c5E6?1<@8bj(bX8e?lgI YBgLoW!zbj*i9E~A?QQ-=K!xf34Zlbrpa1{> literal 0 HcmV?d00001 diff --git a/oura_api_client/api/__pycache__/sleep.cpython-312.pyc b/oura_api_client/api/__pycache__/sleep.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ab43f977c6d1a5aec8fc12bec9c0331848eb0ef GIT binary patch literal 2080 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 K=HCQZl-fUU$^9_^ literal 0 HcmV?d00001 diff --git a/oura_api_client/api/__pycache__/vo2_max.cpython-312.pyc b/oura_api_client/api/__pycache__/vo2_max.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8803ef9a6b62496503304ac592870f33997ea1fd GIT binary patch literal 2107 zcmb7F&2Jk;6rcUfdL5@Uc1S|fYE`umQAyyL{lewf*H zi9;gw5TTrqdP*dOepzOJ(9ZL6I zFtS`IT4q2&tM8jWUGS;_We_;@z?+#h*B7W?@m!yV$=ILuENVmW@KUvGTgGQq%4$Z% zWTxX67m=j@6idH2$P*NZ{ zNmn2}$&zTIBwEV0sH-(DtA=uX!MRZN1Cs>?W)&*bwPH(k>3U!U-Wqj7nbDxi+@jdN zBr2>1BG$4&?2>>36^%`9liw6NXl{v{WR>r%_4I0|n!PEg=cf2Qzu%7Dl7fNGq9v5c zrt~fM6}L#TqB#nOsakv7rGfTfenxZ5b;5JSzWie#QW7fP-?(yYB!(hI|XYe7F}gF|xA!Hqi=M3i#ksM6;ar{# z0~lF>r9rU_3MHNxLHjinuY&LqIiL|61s!J2D)q+1OL(B+H)cRQBFzlRjP(_MXsZ52 z-8mS%*%G>ipJ<$$+WQ2=filJLt;`B@b932bs2GN8I@B;i)i4~-s@iA|8t`nJcD%yy zstg~(P=HopSk0(m*ce)v9-BmgnMWUwl4418QDB6>24b5uWis>DVgAP9<=6jAj7Zmh z%}nmyuitng*57VixZcR*ni9-HRC;3X&3d|S)yEqXZ#0Idn*%tnkdX^}i;riUxVNbi zb*z;{E0>b4J{6h-)U)I(m@{YHkUB z>=?Ftc=7ZyAVqCI*Y4dI4NNl}IUQLI%C;Z9Jux6BL3*LQ6N7%6O~H0-8D9g@6gZB1 dPA(mh@gtIdPCh&$H;?5j+|?cZF9Iq`?{ChV6(0Zq literal 0 HcmV?d00001 diff --git a/oura_api_client/api/__pycache__/webhook.cpython-312.pyc b/oura_api_client/api/__pycache__/webhook.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..03dde480a97c455d00eabe1efcbd936b9a735be4 GIT binary patch literal 5441 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 literal 0 HcmV?d00001 diff --git a/oura_api_client/api/__pycache__/workout.cpython-312.pyc b/oura_api_client/api/__pycache__/workout.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4566d4b1ad49508132a4d55f68a734483e1f492a GIT binary patch literal 2114 zcmb7F&2Jk;6rcU{dhIkcc8Eh0SXKRq)6|yQf`p4IKvV^!1}SNUP+K zI5<)dIpl=YQz9WGA2<|+Lyv`j0S>V^5Umsyi4zhBBZo^*ytljlXb442l=m_7X6L=% z`_1^ru`!jvDE;x%+V>J6zoF9~;D@9438-CS5|gut&6PQuFY_Gwyd~ITSqyE#l5Dvw zhqh=XY^AK&$#OFEB}=ta@85_{T2W0B4DMb=UD|kd7zrx+b+Cd}5_hvrPTd24zjX zZZO04D$$9NOvX-c1&m!nNtrWAnNJe@Gm|Dy1u9aB%2j?EBJkf7PMH&7P6}5gny4yJ zo>W$#K*^M7vMQR&wpdo1TwViGuqbu27UsAEQ!>d?=9-FG*r6UdDE4dztD zI7?L65CpAff!HNJ2PzybuEn>6KA>A-i>&kgwV__`H*`1!Ep3V4@%wS~mgFb;i>6Q` zE$JKXOKzFuMI#4?saiblP+$8jf?qS;O2ej(?-kU+`a5jZD-Ar{G_1FzEo1E()~;1u zrul1Bt5a5U&BAkA`mmI=+m89);X~|7+C79Y{CM41tvLoVY9zg-5ZV(No}g1n>k~_> zxQ=hsoSL&bEZXpmVYZu7#fC>&#kDM2!Ly1nIKVc3vT)~)b8yi$- z6bS113~|rPf#^}I%F^JlF%)M|oCOgmEGD`qgBvyC*{$N5YttfYI1XjusYSQJjCyla z=|;V#N2eCS>?c>KH-pJl>g#c``k<4WvoRQjQ9x-Id^-o?K$-8sH%O4`pR3o* z$|!9IbiW5-y97v$@h8T5{ECcD1F5;M$k+USd_lH&e?Wbc2U_znt%c1W29!fi8=41Q zw&<|WF}X*`^G|M%|qKXMJk1JccsY;L6WXD+qBXxj(r8$Dsr`kBu7 z<-Ii!2g+rJ4`yCiTwKhj0!7yy!=}0(sJd>uX2U`|t;5@GSkVf@Tp2!wfdIFKVL!t= z!*Xb0eQX8=mL7gQtcs=3MS&6C28eCamC3a?4vW_hFU+MY2Y)^O2UFl3LbQ3tQkjdG-<%f5>xVNhkb-I^At1u?bJr=qI z)RXivswfXUeK9{4$bPd9>>8;=!8g#a*)%LC$l=u;xvT}atc4Z$&0~1ugI7;315(uT z3i0)g(7-Z-$&--{zh-&i^AiE`JV?)!w<6GQu}iQWFO8>x=n5RiJtgOl$n+5@J|*uT SksHVIMQ(0q{VxJ4Ebnh7O(J0c literal 0 HcmV?d00001 diff --git a/oura_api_client/models/__pycache__/__init__.cpython-312.pyc b/oura_api_client/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5f738c6af55b22f5b9503ff3ed2b847274a8c2cd GIT binary patch literal 213 zcmX@j%ge<81Xgd(WM~2D#~=<2FhUuhIe?7m3@Hpz43&(UOjXJ*i6w~&x%nxnImHTT z`9%u;rA3JfjsczuMXALF`FX{u#d?04jJMe1<5TjJjS5}ArmEXa&c&dE&8E76Bop&uWgnU`4-AFo$X`HRB_C|H`4YFESt XG!o>(Vvy@TFf%eT-eS-!VgYghgrhu9 literal 0 HcmV?d00001 diff --git a/oura_api_client/models/__pycache__/daily_activity.cpython-312.pyc b/oura_api_client/models/__pycache__/daily_activity.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e3839092b7befbb72fc84be633a26847e8827c2f GIT binary patch literal 4084 zcmb7{%WvD*6^BWQdP|lcvMs+7`4wAnY&o%=IGM>zGmeuuiEW@wF@b=BV8ok<2p@52kk$fJ2?3x48G%JPUclk z$O@V->!ZhB)vpDz0nMNF^PGpf&&k5?IoU_}AG|GJ*&v_u#~Y!FL?tvb@Y?J->yU&3Zi?5oC|-{ZaVA)8eu|;wJ&w_bD8@RzG76k`VC^ zfen}d!vq;7Od?-ZF#Xx5v7Cva`cmb4wp*m*)%;Dm5Z zbbc!xb%LU(OBxYHCoGDZAs18}M@8{fK~h~yKon&oCyFLMd@~B-_@1kVWIJ(Dlyu#& zC46@l5HTb$5X~;cyq0Z#&z-=K`{h9h+*1Mi+=nJ6J{7c<0rr{5r%0;Hu?!N zOq5}QG`{(C$VuFBAN87{+otlYU>l~j<@9QV*kbEt*pm5z*w**F=Kde z60<1g;jKC`ljL9u4l&xC2*jKF4vEuio06g{`U~;7NnRC*o+~=>4|BVQTF^*3X2$WI zbffg3(AXQn5x#ZD3*l}i@bCi4MU)iEILZXd01ADxNgNHg1!L6u0|-RT?0ikG-`Z-< zZr6o@O0e2@7{dsezFb?b-+0iR-Y9LCpH^}QziS8^$CI>|= zKXK+-G|jq9m|?mZCd}MdgkgFZCc-dLhUsOPD8s}UrjKD_4AaFh35Mxnm^j1qGfbRe zx*2AGVY(TnhhYYRNxILlhhcgdW{C0A%P@TmGt4l343l7(5r#=HOh3bnGE6_i3^2?X z!wfLYAj4c>m_dd~GR#GWNixh3!=xByh+&2qW}IP$ks%YXi$>^Z5=NsqDtgjmPVZf; zH|-qQ>Qy|7yyZl4s$^N>iU{S|90lC*SvkWbPMp>zBIYF3z(VizNs#%Z7ev$`W4guM ziRPGe>Wo0*bDITmPf}lEHk6?clJp#LhVsgb{D+En#!K&66HTr?ir%h{BD5c-Z8=HR z*z4?|=ZuA(KcW#?DQKPf#Can<#S3@lbb}Dn5^Y0l6$B@y8@jkhB(s(EqbBqjcxUvS zev8OZ)+MM3>~rb*I88C1Ws@DKC@q&CA8O2D>#}*SwP94ETUXRxrVZYvW!Cek`)|XN4bHr zigFX>7Ro0m7;ENjlmJQ)m!+W zy^C$@wHx%@t6;tc=U%KPYm8> zWgk-gBGd zS7=39gMSNCF>H=-S3N%dE*(u7BZYwzx@0WI! zj^8fsK$>cOx6sT$w~$>KFKX{JwI^;qPgepMS_#Zk}@Zd)j-MPgaIbIrurv@*aM=vhgnte(2Kw4LB+X AmH+?% literal 0 HcmV?d00001 diff --git a/oura_api_client/models/__pycache__/daily_cardiovascular_age.cpython-312.pyc b/oura_api_client/models/__pycache__/daily_cardiovascular_age.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2f940922fed4409a742226a8ff276e0e28ea7518 GIT binary patch literal 1343 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 THzhDkPwLvaarh5`K_&SwO_X&I literal 0 HcmV?d00001 diff --git a/oura_api_client/models/__pycache__/daily_sleep.cpython-312.pyc b/oura_api_client/models/__pycache__/daily_sleep.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..02bb782bbb8d7c6e25735a0092f141ab48fd0449 GIT binary patch literal 3846 zcmZ{n&r=&m7RP6#)^7v|#BZ>`{IGP~y&p0qpe76pfJM^MP$1;s_hpPY7Eaj#`{bE^kgeU{bBtv+P+=l!w3ukd2b z!F;!5oo;3osxsGFimD2?kXXj52n~4+ZqV7Ikp&rF?gWnqrL5vqM1Tike_z>elj2kh2#JCaT zK}-NK9>jPN(}EZ;Vtj}RBE|q-Z9QXC9SLG8B9oP1_&~XjC_z?y4E8>&(r+Kq~W$1&pjAt2Pm9WnM|j z$Z0A`%r8mltg@>@xkZxxvMa0h6AeC<765T`Va`;>U~Q5lXEMAg_}IE6X|3?Q4Wu0; z1kwT0$>m4k9arJE+tbNxnoeuGnGDsYUuL!E)7f23&gJneGsx zQuDUYS95s{Hi;$RkNyuXKM3{V@v?Gs#T;HP+%0-bod>}hSw7!5bg?{F$(uuqg*$tX zi;4XwHL_TbjFlf84VaOo!p7dKl5_vt8d<83Oq7kIC3ECv;a;(~Gk!!yvO>$G~Z~A%u%5UWpi&s8%Qlf~k4*0qt_DZL2UUFJC4=*)W zwpVc}?w>tpJH$u*G+^xzqBK`6FkZy8aK_VMnky0*A7X-tu~sDCX~6gq)5@8)Mmzq- z$_6HYn0Dk(05L6y2_dEhF+s$1puU2LX+=yYVpKn5?h{z#Cj#;GD zXM-F<cqu zl)_pPC>k)QBJ*<|BC2l4>D(4`>4~gHHJ&jDEU-frcqGw`!h+2T2Q`DW?r2mtl9K#_ zYVtG6I^RbXN#W-(oaNR*&N`BmzaViFJ)=X&rewYcmw0%w0ssrE zsb@*UKCaK&Wg-5Kv?Ch^)$)?8QtnR|cSlJdeODY{PdJGH9B6B^$dMZ49C7L(_|tHMeX4n+=VBBF;A+ z#$s9+;zbuoH+)K`ZfIIBRIPyLfsz-*3dk3s1OZU%0T~4u0~zPSTu;@kY-kf;utM`C zC`CbHAXcQFgwka$jO=8ys&)nHQy^DCra@*v{si&~$SlYl$UF!nrP`+;*FY9P7D29q zz-F_9-~g0{ngSVu{#Tye{UDrp1K-~{=@_k%(Gy?r`;C*fo*LLK z*~6(Cxo?f5dVSN3ZrKf!gTK|tR{iqKQMkJFz`XphO8QC*<&{G|7Y`fsmKo)4fOnmT zJm1-F4EwG*aj#0k#bil8NY}`{`lTzC2i3*T%}bkh+s=b;Yh?5HNeiSmf8bt!q)aMn zX8&wqbuU_+-RC~f)<>h2@X-@aEA-%s2L+=PJ(Ei^u)E zm2c|yv#-sWNA_64W%_#HxR*bBbbea)2j5`olcTxn^4I1RZwIF8_Te%#F}v_4T)n+x z&L*lPQtqvczmAwBVXsSAk{={zPHuqf9jx|`lQS!Lxa<^7am3x!L%j`~m7?VxF~U0D zAK+h6J;$?SYW)bwJd4lJSB7L{pVLgt_t9>*x01C*tSx_&_iu0S&!F_tR$2xd2)L=k z?&4d?-@5nRv7di~$jR_nGYdA!m_1aWw_*Y2&E*w2W26!qjMX|z_|HK;$A1oNAFMOQ zI%ljC$2xl~zpPF_bmTYh4g06z&)O<)jsMCCI-CUmb&8_+Rv3RPjJy>_Pu(8zmRRzg e3UEISM?^&|rcMR8pRS4yalF*`KY`yYPyY`mCDGOZ literal 0 HcmV?d00001 diff --git a/oura_api_client/models/__pycache__/daily_spo2.cpython-312.pyc b/oura_api_client/models/__pycache__/daily_spo2.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3550254752dc21fcb7b415909a4343e6432c58a0 GIT binary patch literal 1426 zcmZuxOK;so6rS1bdEWCX zkFkWulCi4ASe%Msgn5Ut?}t2UW)!xj+6WFnWq%i?yr^aw<4KYh9JiM<_JE9GmOQ)K zznR9euZBq?RsV-n-Rh@9#j|ocFrI~MGYVx=^kG#Q<$ZzOvph>z-^)sczH7iG^9ulX z$m5GkhqgaDUz^5)xwE!=eI)O)ni#gJ&K%SKU(=sqdRkLyN=r~w4wSSS4MAr|n8JEw zpXlsLPx@MCOIXOBaGWVl3pniw$C=`|Q=ATPx)Y9zoYDm8XHNWTJ{%(yl__|+Ud^Hk zER$*z^Z~cIbC+Uwgdr?h}vL{{E@O+V|czb~Z-W z_vAe`CZ26PeU)6HpVB|Q&ig!+AhF7NsO J5(v7<{{WLDK6n5C literal 0 HcmV?d00001 diff --git a/oura_api_client/models/__pycache__/daily_stress.cpython-312.pyc b/oura_api_client/models/__pycache__/daily_stress.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9bd56e52be683ea0fa3ca1bff37ec7ed9d4f271e GIT binary patch literal 1493 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 literal 0 HcmV?d00001 diff --git a/oura_api_client/models/__pycache__/rest_mode_period.cpython-312.pyc b/oura_api_client/models/__pycache__/rest_mode_period.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..defa20c067f9dc800f76a86d1625c1c5abf4dae7 GIT binary patch literal 1567 zcmZ8h&2JM&6rb5I@2BE}=KVZ>*6TKb z@yWk`ZCpG?$ln;uhCX%1_rQ5b0us=O#MGs+;wltlB~oL})neV%WvoUgHjZ_z@z3pEF^i&BK_j7hf$bod)9?I1dRUE)9sQ^eL=s-ey^j)j#Wo8B85V z6{hDUe#|^i*q#@sK`%nT=6OH#{HVzMM#fn5^;&x)jai%bl7#X0k15}5r#`{l{= zyP>xpg)GV2P>w}eo5NSJH1oQQhiULZx6iR3(iq@m6)+FTiz`I8cypFHbSw=}9OGAD z4vuw)FqLTmy$mSR!Bfyv2r~lpcWrli8E%D{kW=5}^uU0u0#q2T#cXEIg9jX=#Ho}x zRp6*oPNl>#OPm^T>Qjzc;#eh4qr|aF9J|D60_WJYj*XnY?o`Fxta)F_OT;!tLJ0$* z27X`IS?=@PLpeB9=zbLXSx1;G2_z<(2TcGbsAujwP+Alw%-dkNk|S6<0Qo&J|0c>d z!^F#34E^)-9%s&5ybAl`s4-p#Bh)PCqUm{llB7@{l$v=S$G&oG9e)RniRJ_v>^eV* z2G1$fFvM&rdIo8mA_z za>m$R8<=CF=*CNfEIA{yHdq6E$=LqlmyN;Jlg46^G|9>ZNbbG84A~W9`_|y=;rhis4Iq(RFWIcT_$oLXuj>=j}Krh4{D6YXk##2m6xvw~yc=xa@AnL;1 zGxzlaS6u4?dMz}vGiT5{a{Xr!LvsUu88YDd?Z#g7+!MR? zRBMeEm+<0;b365C+EQ^|q<28OX15>sNgl3qJg`)RF!FvkOzsw&kYAPZkdl$FgY38b z>fxQdHZK?N)D^x0)hPV3kHEZ8DW&^ld7qr!C#{L`9<>JHgupcUl+IIQ=u8MqldDvr N%foN}BQVKr{twiYYk&X% literal 0 HcmV?d00001 diff --git a/oura_api_client/models/__pycache__/ring_configuration.cpython-312.pyc b/oura_api_client/models/__pycache__/ring_configuration.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e3c39df6c18f50d491a7d863b04ec5ae3b07a508 GIT binary patch literal 1581 zcmZWp&2JM&6rcV6u#Hn1qC!AQN}OVpVi69FD%3-Z2qkHdkg5t_MicMg9c4e<*>%C3 zBDIy$n>p3T9$V4>gdT#(0gY5qsp_Fp54IdGocdzUtk&~rd?usis_V?c8Tehm_9I#nVMVTcqL9nsre-}uf(jB zm{nlbW@?oZr&{8i0#1F#sY=d3Z`Q=ZU6yn|OOyMo+vh~?WU&L?Ercof=E+@SZHePr<%_vq*J7du|l#5!HQxfGN zE;Q_bW~`euP2ud4h$I~o1Y zfvb56JzCnTmA3m@Y+4f>I^s~CUX zC($foWH}dgj7gHDx!RcwV=gO*S0zA)Anr;yBjKEcMF~q190|(+!eB}M3z~w7UT?R; zG^Q=yPZG*o4^qC@N@3f3gYtk$kKs6uH*Qx$Cl~a z;|j7W2e*b_jdjC2Ju$2W;951{dX_zzM<1;HeB*HA_R;Frp*cScN7>`}xw$o|&P49K z5jj2F7{P*uR|k;`Qe=K6vVaG@H&LsJ#9KMHq+9 literal 0 HcmV?d00001 diff --git a/oura_api_client/models/__pycache__/session.cpython-312.pyc b/oura_api_client/models/__pycache__/session.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f0f4253f2a03658a0bcf37291e9c40b34d894a79 GIT binary patch literal 2330 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 literal 0 HcmV?d00001 diff --git a/oura_api_client/models/__pycache__/sleep_time.cpython-312.pyc b/oura_api_client/models/__pycache__/sleep_time.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d4f8bd5cfdd1e78cd76ded52c3e83394fb8a8f81 GIT binary patch literal 2116 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 a|A9fD<2MVu92xS{^IY*ykGR7ubAJJoz2GPS literal 0 HcmV?d00001 diff --git a/oura_api_client/models/__pycache__/workout.cpython-312.pyc b/oura_api_client/models/__pycache__/workout.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ce747be47d4bc3bc2c4e3fcda9ab8ebad6b48b01 GIT binary patch literal 1493 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 literal 0 HcmV?d00001 diff --git a/oura_api_client/utils/__pycache__/query_params.cpython-312.pyc b/oura_api_client/utils/__pycache__/query_params.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d8312c6de2a4595d9fbb0e95a616c7fb20475e6 GIT binary patch literal 2135 zcmbVNO>7fK6rT0|_^oxCEi^#Gfpsa zBr0)8B_x7+z*UjTr4iF4eox!ffTR|1Nc8g9*T{fqc=gU}|fTh#IhCp20;)1VeE zoIQ;xfpx~&+~1HRGL=wsEKW=nipoBN5 zZ!6;acxdoe;n_X~({03e)>?=sVjJ`zmgr~;UDA8n{`)~J7EucoA+xJbJ_!63LOHgu zh~F;>^8lYNo)Kkx7RQWJQMX&cELJ>g}^5}-lXu6Y|KE6fgEK4aiW7Cn`)SGnd>ud`8J`ViDBAh zK~$Ql&=hEx=qG-DGFJ^elA{gZCp5Pj(B)hJ+gfi9zRFhJE!#Dh_&nDDfUumzL$rBp zuHFnM?WnC~*#aJvqqOKS1WAf$(=c5_&&E+|d}HR0^J{W?Yh>#2$aH69dUNF9qtwBz z?8GB&VjIlRLYRTnDVNT|6!D!%%J}38FY{oT_@S}9YC-R9QCx2T1yL5B?G2b-M9hO2 zJp(9Gdb(D^QSPWM^=oBKYiXATJ;t7UD4It%^_zyRy2ws^0+G%G`quO{qh(xEZlqg! z%lJHTV@N}`x@N5DMX*w0c@>!n>s>H^Uoh%rIypcKK@=dF;N1(Pk~omlV$}uaHGFA{ ztCsK7fT(uhd4a#%Cm@>xQ~ek^vp5Dm8w4GDCF+?BHj7WWHOcj$!6h>Bid6%i$E(Fg zy&g~w+;AK?F$Bx|Y+`uDwQ#gY{rq2KOMj2^xG45J%1qQ>k&y)f5BKpc;U1C2ls*OJ zdf>?8lfLtR9P}}j$BP15as%qtMc21PRXdSpW}?n_qn)ZErplJD^9<7n}f>k z9bU4brt!-BGmvz*pzLIRE8^$Jyhz7B@a@EB7wnUt2G%U;b=F>!5v zn<*rW?{%0=bzxz_%!bKw*$M1&IZTPI=B^i?T?TK~0Si>z2vi*MR4|>2IzkojAv+*5 zzK`gLSQ2GDPrc&>PNPOn&^O?(H~`okn7XQ>D8HfkztGelXm%6Lc8#nudH2dg{h|Hc z(T8s5t@k>I3S9)sAF4mGAH8iMcK4&Xx3}KRZQ%n?llzquTcfYHC)(`pe7o71GCQL) zT@|!{4o_}0+QqvE+8=lJ9qJ4p?&_fJ8fa{)z30|YS9}Q#8yWAWg_J=?W_@~{tsnmM L!m})BWcL37&L&H5 literal 0 HcmV?d00001 diff --git a/oura_api_client/utils/__pycache__/retry.cpython-312.pyc b/oura_api_client/utils/__pycache__/retry.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c1cc014996a2b0f7ccca9ccdb688c6ed1f689946 GIT binary patch literal 4881 zcmb^#TWl29_0I0hzSs8pwQFqSF<@W`v7I!5$RZ@Xm8PL8xJlU-E#sY8d+_dzxigDd zYh789B^D*EoT@2Cid08Mtt0xuC(2K&ej&9V1}w2mQCkd;M1&J&rWh|I~J9GBu)?@4)} z_vXBLKE-oH=4Bx#`v? zG09ua-!D+ho|-tO`I49Rlz7?0sDu)4;u@@-tY6(3D+ysz5^CI2QMR(*nIy8e z#MkDIf^4KN*c9ZIDda#tqH<WTFKoXS*)ntBt4QOVFju@0>76<74}bJ_yldY4q*RVn0?}W#pI`sU`h~8A z=wj(cY}d8GuCj0^+)zF>*Z0wzOMGGl79>_sIg?Q)&|9o9>IzQ-o|LY`wXC;hv%u$hiVwWUZZ1W#L{74BSevnX+O+n# z>>Xdn&f{5o-hds89C%7Ec*=eSd>eGHh-y}pH1HQk0HQIb7jiPhC3SQZ%uf<+)i6tA z;AyvU8HdE-G4#9II_8*Uih3r4-XyyQ`{G_!r;jJz}uP^gHjH>S4Vf7R1Jl(9voJ;gPf8+E2J8#W|)$eQ7DEVikhNmjuiE3h1n`; zDGL%!dT9qvFwX&P5CcsH{DQV4X~f=_*;kRS$0_&>Fe9|;b`ofud+EBryVBVDz()ej zbIH&2<$=A+`-hjhf4J;_ztY&^j4yW&Ec*v5;ntgB@p@QXICisd=vv>8mM?QH8F5lI$JPqU&kV=Ur*_w_o)L>N)z$@+ zS}Jhaa2FQPo|qQ8Vws@pJZx4^g3 z1>jTuEW{pE1gL{A3t2(tW&_h)i34?brmpPCm<7mqGUOjyCCm#aA^sX7n74>qV3@PT z4t8m0LPVZ+YaBu1A@QKYf?CCeTu~s2`{T&!9E7tU0QrO>ySv&}z}!V-X5q4D-0K2@ z{?!BD%4;BpPX#RXrt4zmxsc3F!Sp3ZO8d!iNKez=An}lM{t{6kHecj^&Sg9tIZAj^ z@_fi`tc%N=2pvZQhBZ;PtU;aPJr}x!&I--oJ|w1FX+WLw?bzH=;@6DL(Y42?1+Brv z{K*>ZP1a+BnY z_ZXSvlHS92k<3o=K}%p++6o@11!op|C>j(CVYC}DOtKblLXBt%Mov*Cl3|ONphmKM zhWO)o zp$f#^^QV4M{3_A)>(GVJ!t2Y4B0h8h?0b-Up!GveaF6fG7s_(g^hgFk-O8RDoJZz2!d#eP&tE4GjZeYoQ zqU9$AiV9m*@zH+dMo&2b%}Hd<-`b25 z5@}oFTkW(2#BRgeV_U(rbe<>#o^}MEbt&-f+MgVhJqn~DMUZ*;1jPprHXl6T{O}C( z7X!(V6+VKg2i`zgb#$uN?x6_9$j4q^?DP=h$E4G$PW#uow&hnee0s?lw|IjyVR46| zTKO(JAQXWl4Vc|ZPl!-QW2 z1wkdp8%i#_ma-`l)V*SCKMSPODrh8~o@%YLr~caLY2?`o08O2AZ7K81fkb89rZWF= zW2IvQbPbjEF6hFQ`1&&cvk0?yIO1KjgMY#Yt@@UO9DaLvI2o{DPff}z>9iF}r}Mg8 z$RQp{r%yp%cV_(Qw5(@f*sE%$C1i8DWLo@)uIK3Muq1sM0hWC18uS&!5E%Hq3t*Pq z;$Nx;NLP2ItEbZ0Q;Bz05?%MAZ9>oOwnSBc9->*>x~dg#dFE?L&Y?Gz&fUJ)ge&X`--#v(^3%E;j#8l|Xmd%ep^qWZl(GD>zsk zB8iSlq7y~i62BLGjccpKTB}~@Z#T771?WM_A0011UH8%hN literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_client.cpython-312-pytest-8.4.2.pyc b/tests/__pycache__/test_client.cpython-312-pytest-8.4.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dfc02bdb010ff575997bde79860233618948fc48 GIT binary patch literal 110535 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 literal 0 HcmV?d00001 From 1dbe4ca30a1e121c11fb57b69ffa7eded2aa98ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Sep 2025 05:56:59 +0000 Subject: [PATCH 5/5] Add comprehensive branch cleanup recommendation document Co-authored-by: godely <3101049+godely@users.noreply.github.com> --- BRANCH_CLEANUP_RECOMMENDATION.md | 60 ++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 BRANCH_CLEANUP_RECOMMENDATION.md diff --git a/BRANCH_CLEANUP_RECOMMENDATION.md b/BRANCH_CLEANUP_RECOMMENDATION.md new file mode 100644 index 0000000..91063c8 --- /dev/null +++ b/BRANCH_CLEANUP_RECOMMENDATION.md @@ -0,0 +1,60 @@ +# Branch Cleanup Recommendation + +## Executive Summary + +After thorough analysis of 3 unmerged branches, **all should be deleted** as they either remove valuable functionality or contain incomplete/broken implementations. + +## Detailed Analysis + +### ❌ DELETE: `copilot/fix-13` +**Status**: Tests pass but regressive changes +**Issues**: +- Removes pagination utilities (`stream_paginated_data`, demo files) +- Removes HTTP method support (POST, PUT, PATCH, DELETE) +- Removes webhook credentials from client initialization +- While it adds some tests, the cost is losing working functionality + +**Command to delete**: `git push origin --delete copilot/fix-13` + +### ❌ DELETE: `copilot/fix-b142c2c2-d3a9-4637-bbc2-0ed979d16113` +**Status**: Tests fail due to incomplete refactoring +**Issues**: +- Massive structural change moving all code to `src/` directory +- Import errors prevent tests from running +- Contains Docker/CI improvements but incomplete +- Represents abandoned refactoring effort + +**Command to delete**: `git push origin --delete copilot/fix-b142c2c2-d3a9-4637-bbc2-0ed979d16113` + +### ❌ DELETE: `feature/repo-organization-and-devops` +**Status**: Tests fail, redundant branch +**Issues**: +- Contains only 1 commit that's duplicated in fix-b142c2c2 branch +- Same import/structural issues as fix-b142c2c2 +- Redundant - serves no unique purpose + +**Command to delete**: `git push origin --delete feature/repo-organization-and-devops` + +## Current State - Keep Main Branch + +**✅ Main branch is healthy**: +- 132 tests passing +- Complete error handling with retry logic +- Pagination utilities working +- HTTP method support for webhooks +- Full API coverage + +## Recommendation + +1. **Delete all 3 branches** using the commands above +2. **Keep main as single source of truth** +3. **No merging needed** - main branch already contains the best functionality + +The main branch represents the most complete, tested, and functional version of the codebase. + +## Implementation Notes + +- All branches were tested for functionality +- Main branch has comprehensive error handling, pagination, and API coverage +- No valuable code would be lost by deleting these branches +- Future development should continue from main branch \ No newline at end of file