From 8aaa295e4a922635f5a7bfc23ce33e19cbf04149 Mon Sep 17 00:00:00 2001 From: Jon Stroop Date: Mon, 10 Mar 2025 14:51:48 -0700 Subject: [PATCH 1/2] Doc updates and remove unused imports --- docs/DEVELOPMENT.md | 174 ++++-------------- docs/RATE_LIMITING.md | 13 +- docs/TYPES.md | 7 +- fitbit_client/auth/callback_handler.py | 7 +- fitbit_client/auth/oauth.py | 3 - fitbit_client/client.py | 5 - fitbit_client/exceptions.py | 2 - fitbit_client/resources/_base.py | 10 +- .../resources/active_zone_minutes.py | 2 - fitbit_client/resources/activity.py | 3 - .../resources/activity_timeseries.py | 2 - fitbit_client/resources/breathing_rate.py | 1 - .../resources/cardio_fitness_score.py | 1 - fitbit_client/resources/electrocardiogram.py | 2 - fitbit_client/resources/intraday.py | 1 - fitbit_client/resources/nutrition.py | 2 - fitbit_client/resources/sleep.py | 2 - fitbit_client/utils/pagination_validation.py | 1 - .../activity/test_get_activity_log_list.py | 1 - .../resources/nutrition/test_create_food.py | 1 - .../nutrition/test_error_handling.py | 4 - .../sleep/test_get_sleep_log_list.py | 1 - tests/fitbit_client/resources/test_base.py | 2 +- .../resources/test_docstrings.py | 1 - .../resources/test_pagination.py | 4 - .../utils/test_curl_debug_mixin.py | 1 - .../utils/test_date_validation.py | 2 - 27 files changed, 49 insertions(+), 206 deletions(-) diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 4ade288..ad81f49 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -7,6 +7,7 @@ - Python 3.13+ - PDM - Git +- ASDF (recommended) ### Download and Install the Source Code @@ -49,8 +50,9 @@ fitbit-client/ │ ├── resources/ │ │ ├── __init__.py │ │ ├── [resource modules] -│ │ ├── base.py -│ │ └── constants.py +│ │ ├── _base.py +│ │ ├── _pagination.py +│ │ └── _constants.py │ ├── utils/ │ │ ├── __init__.py │ │ ├── curl_debug_mixin.py @@ -59,26 +61,22 @@ fitbit-client/ │ │ ├── pagination_validation.py │ │ └── types.py │ └── exceptions.py -├── tests/ -│ ├── auth/ -│ ├── resources/ -│ └── utils/ -└── [project files] +└── tests/ + │ ├── fitbit_client/ + │ ├── auth/ + │ ├── resources/ + │ └── utils/ + └── [project files] ``` -## Goals, Notes, and TODOs - -For now these are just in [TODO.md](TODO.md); bigger work will eventually move -to Github tickets. - ## Development Tools and Standards ### Code Formatting and Style - Black for code formatting (100 character line length) - isort for import sorting -- Type hints required for all code -- Docstrings required for all public methods +- Type hints required for all code (enforced by `mypy`) +- Docstrings required for all public methods (enforced by `test_docscrings.py`) ### Import Style @@ -102,7 +100,7 @@ import typing import datetime ``` -The one exception to this rule is when an entire module needs to be `mock`ed for +The one exception to this rule is when an entire module needs to be mocked for testing, in which case, at least for the `json` package from the standard library, the entire package has to be imported. So `import json` is ok when that circumstance arises. @@ -120,46 +118,29 @@ Follow your nose from `client.py` and the structure should be very clear. #### Method Structure - Include comprehensive docstrings with Args sections -- Keep parameter naming consistent across methods -- Use "-" as default for user_id parameters -- Return Dict[str, Any] for most methods that return data -- Return None for delete operations +- Keep parameter naming consistent across methods (see [Naming](docs/NAMING.md)) +- Return `JSONDict` for `JSONList` for most methods (`get_activity_tcx` returns + XML as a string) +- Return `None` for delete operations ### Error Handling -The codebase implements a comprehensive error handling system through -[`exceptions.py`](fitbit_client/exceptions.py): - -1. A base FitbitAPIException that captures: - - - HTTP status code - - Error type - - Error message - - Field name (when applicable) - -2. Specialized exceptions for different error scenarios: - - - InvalidRequestException for malformed requests - - ValidationException for parameter validation failures - - AuthorizationException for authentication issues - - RateLimitExceededException for API throttling - - SystemException for server-side errors - -3. Mapping from HTTP status codes and API error types to appropriate exception - classes +The codebase implements a comprehensive error handling system. See +[ERROR_HANDLING](docs/ERROR_HANDLING.md) and +[`exceptions.py`](fitbit_client/exceptions.py). ### Enum Usage - Only use enums for validating request parameters, not responses -- Place all enums in constants.py +- Place all enums in [`constants.py`](fitbit_client/resources/_constants.py) - Only import enums that are actively used in the class ## Logging System The project implements two different logs in through the -[`BaseResource`](fitbit_client/resources/base.py) class: application logging for -API interactions and data logging for tracking important response fields. See -[LOGGING](docs/LOGGING.md) for details. +[`BaseResource`](fitbit_client/resources/_base.py) class: application logging +for API interactions and data logging for tracking important response fields. +See [LOGGING](docs/LOGGING.md) for details. ## API Design @@ -190,23 +171,10 @@ client.get_profile() client.get_daily_activity_summary(date="2025-03-06") ``` -Method aliases were implemented for several important reasons: - -1. **Reduced Verbosity**: Typing `client.resource_name.method_name(...)` with - many parameters can be tedious, especially when used frequently. - -2. **Flatter API Surface**: Many modern APIs prefer a flatter design that avoids - deep nesting, making the API more straightforward to use. - -3. **Method Name Uniqueness**: All resource methods in the Fitbit API have - unique names (e.g., there's only one `get_profile()` method), making it safe - to expose these methods directly on the client. - -4. **Preserve Both Options**: By maintaining both the resource-based access and - direct aliases, developers can choose the approach that best fits their needs - \- organization or conciseness. - -All method aliases are set up in the `_set_up_method_aliases()` method in the +Method aliases were implemented because yyping +`client.resource_name.method_name(...)` with many parameters can be tedious, +especially when used frequently. All method aliases are set up in the +`_set_up_method_aliases()` method in the [`FitbitClient`](fitbit_client/client.py) class, which is called during initialization. Each alias is a direct reference to the corresponding resource method, ensuring consistent behavior regardless of how the method is accessed. @@ -214,53 +182,16 @@ method, ensuring consistent behavior regardless of how the method is accessed. ## Testing The project uses pytest for testing and follows a consistent testing approach -across all components. +across all components. 100% coverage is expected. ### Test Organization -The test directory mirrors the main package structure (except that the root is -named "test" rather than "fitbit_client"), with corresponding test modules for -each component: - -- auth/: Tests for authentication and OAuth functionality -- client/: Tests for the main client implementation -- resources/: Tests for individual API resource implementations - -### Standard Test Fixtures - -The test suite provides several standard fixtures for use across test modules: +The test directory mirrors the main package structure within the `test` +directory. For the most part, the naming is 1:1 (`test_blah.py`) or otherwise +obvious--many tests modules were getting quite long and broken out either into +directories or with names that make it obvious as to hwat they are testing. -```python -@fixture -def mock_oauth_session(): - """Provides a mock OAuth session for testing resources""" - return Mock() - -@fixture -def mock_logger(): - """Provides a mock logger for testing logging behavior""" - return Mock() - -@fixture -def base_resource(mock_oauth_session, mock_logger): - """Creates a resource instance with mocked dependencies""" - with patch("fitbit_client.resources._base.getLogger", return_value=mock_logger): - return BaseResource(mock_oauth_session, "en_US", "en_US") -``` - -### Error Handling Tests - -Tests verify proper error handling across the codebase. Common patterns include: - -```python -def test_http_error_handling(resource): - """Tests that HTTP errors are properly converted to exceptions""" - with raises(InvalidRequestException) as exc_info: - # Test code that should raise the exception - pass - assert exc_info.value.status_code == 400 - assert exc_info.value.error_type == "validation" -``` +All resource mocks are in the root [conftest.py](tests/conftest.py). ### Response Mocking @@ -332,39 +263,4 @@ git commit --no-verify -m "Your commit message" ## Release Process -This section will be documented as we near our first release. - -## Pagination Implementation - -The pagination implementation uses the following approach: - -### Pagination Iterator - -- Uses the `PaginatedIterator` class that implements the Python `Iterator` - protocol -- Automatically handles fetching the next page when needed using the `next` URL - from pagination metadata -- Properly handles edge cases like invalid responses, missing pagination data, - and API errors - -### Type Safety - -- Uses `TYPE_CHECKING` from the typing module to avoid circular imports at - runtime -- Maintains complete type safety and mypy compatibility -- All pagination-related code has 100% test coverage - -### Resource Integration - -Each endpoint that supports pagination has an `as_iterator` parameter that, when -set to `True`, returns a `PaginatedIterator` instead of the raw API response. -This makes it easy to iterate through all pages of results without manually -handling pagination. - -## Intraday Data Support - -This client implements intraday data endpoints (detailed heart rate, steps, etc) -through the `IntradayResource` class. These endpoints have some special -requirements if you're using them for anyone other that yourself. See the -[Intraday API documentation](https://dev.fitbit.com/build/reference/web-api/intraday/) -for more details. +_This section will be documented as we near our first release._ diff --git a/docs/RATE_LIMITING.md b/docs/RATE_LIMITING.md index 53eb1e9..e04205c 100644 --- a/docs/RATE_LIMITING.md +++ b/docs/RATE_LIMITING.md @@ -48,9 +48,9 @@ client = FitbitClient( redirect_uri="https://localhost:8080", # Rate limiting options (all optional) - max_retries=5, # Maximum retry attempts (default: 3) - retry_after_seconds=30, # Base wait time if headers missing (default: 60) - retry_backoff_factor=2.0 # Multiplier for successive waits (default: 1.5) + max_retries=3, # Maximum retry attempts (default: 5) + retry_after_seconds=10, # Base wait time if headers missing (default: 30) + retry_backoff_factor=1.5 # Multiplier for successive waits (default: 2.0) ) ``` @@ -70,9 +70,10 @@ The client uses the following strategy for retries: With the default settings and no headers: -- First retry: Wait 60 seconds -- Second retry: Wait 90 seconds (60 * 1.5) -- Third retry: Wait 135 seconds (60 * 1.5²) +- First retry: Wait 30 seconds +- Second retry: Wait 60 seconds (30 * 2.0) +- Third retry: Wait 240 seconds (60 * 2.0²) +- Fourth retry: Wait 240 seconds (240 * 2.0²) ## Logging diff --git a/docs/TYPES.md b/docs/TYPES.md index 4b9c04d..da32dbc 100644 --- a/docs/TYPES.md +++ b/docs/TYPES.md @@ -2,9 +2,10 @@ ## Overview -Strong typing JSON is complicated. The primary goal for typing in this library -is to help you at least understand at least the outermost data structure of the -API responses. All resource methods (API endpoints) return one of three types: +Strong typing JSON is complicated to do in any meaningful way. In our case, the +primary goal for typing is to help you at least understand at least the +outermost data structure of the API responses. All resource methods (API +endpoints) return one of three types: - `JSONDict`: A dictionary containing JSON data - `JSONList`: A list containing JSON data diff --git a/fitbit_client/auth/callback_handler.py b/fitbit_client/auth/callback_handler.py index 190e6e3..5e56b37 100644 --- a/fitbit_client/auth/callback_handler.py +++ b/fitbit_client/auth/callback_handler.py @@ -5,13 +5,9 @@ from http.server import HTTPServer from logging import Logger from logging import getLogger -from socket import socket -from typing import Any # Used only for type declarations, not in runtime code -from typing import Callable +from typing import Any from typing import Dict from typing import List -from typing import Tuple -from typing import Type from typing import TypeVar from typing import Union from urllib.parse import parse_qs @@ -20,7 +16,6 @@ # Local imports from fitbit_client.exceptions import InvalidGrantException from fitbit_client.exceptions import InvalidRequestException -from fitbit_client.utils.types import JSONDict # Type variable for server T = TypeVar("T", bound=HTTPServer) diff --git a/fitbit_client/auth/oauth.py b/fitbit_client/auth/oauth.py index 7cdc492..684a605 100644 --- a/fitbit_client/auth/oauth.py +++ b/fitbit_client/auth/oauth.py @@ -21,11 +21,8 @@ # Local imports from fitbit_client.auth.callback_server import CallbackServer -from fitbit_client.exceptions import ExpiredTokenException -from fitbit_client.exceptions import InvalidClientException from fitbit_client.exceptions import InvalidGrantException from fitbit_client.exceptions import InvalidRequestException -from fitbit_client.exceptions import InvalidTokenException from fitbit_client.utils.types import TokenDict diff --git a/fitbit_client/client.py b/fitbit_client/client.py index a63feb6..634eccd 100644 --- a/fitbit_client/client.py +++ b/fitbit_client/client.py @@ -8,11 +8,6 @@ # isort: off # Auth imports from fitbit_client.auth.oauth import FitbitOAuth2 -from fitbit_client.exceptions import ExpiredTokenException -from fitbit_client.exceptions import InvalidClientException -from fitbit_client.exceptions import InvalidGrantException -from fitbit_client.exceptions import InvalidRequestException -from fitbit_client.exceptions import InvalidTokenException from fitbit_client.exceptions import OAuthException from fitbit_client.exceptions import SystemException diff --git a/fitbit_client/exceptions.py b/fitbit_client/exceptions.py index 7959dbe..c3e9d69 100644 --- a/fitbit_client/exceptions.py +++ b/fitbit_client/exceptions.py @@ -1,8 +1,6 @@ # fitbit_client/exceptions.py # Standard library imports -from typing import Any -from typing import Dict from typing import List from typing import Optional from typing import TYPE_CHECKING diff --git a/fitbit_client/resources/_base.py b/fitbit_client/resources/_base.py index a7b14e4..be644cf 100644 --- a/fitbit_client/resources/_base.py +++ b/fitbit_client/resources/_base.py @@ -2,21 +2,15 @@ # Standard library imports from datetime import datetime -from datetime import timedelta from inspect import currentframe from json import JSONDecodeError from json import dumps from logging import getLogger from time import sleep -from typing import Any from typing import Dict from typing import Optional from typing import Set -from typing import Tuple -from typing import Union from typing import cast -from typing import overload -from urllib.parse import urlencode # Third party imports from requests import Response @@ -31,7 +25,6 @@ from fitbit_client.utils.curl_debug_mixin import CurlDebugMixin from fitbit_client.utils.types import FormDataDict from fitbit_client.utils.types import JSONDict -from fitbit_client.utils.types import JSONList from fitbit_client.utils.types import JSONType from fitbit_client.utils.types import ParamDict @@ -532,7 +525,6 @@ def _make_request( retries_left = self.max_retries retry_count = 0 - last_exception = None while True: try: @@ -583,7 +575,7 @@ def _make_request( return None except Exception as e: - last_exception = e + pass # Decide whether to retry based on the exception if retries_left > 0 and self._should_retry_request(e): diff --git a/fitbit_client/resources/active_zone_minutes.py b/fitbit_client/resources/active_zone_minutes.py index 5f5357a..8688fc9 100644 --- a/fitbit_client/resources/active_zone_minutes.py +++ b/fitbit_client/resources/active_zone_minutes.py @@ -1,8 +1,6 @@ # fitbit_client/resources/active_zone_minutes.py # Standard library imports -from typing import Any -from typing import Dict from typing import cast # Local imports diff --git a/fitbit_client/resources/activity.py b/fitbit_client/resources/activity.py index b6abf35..ad0f0da 100644 --- a/fitbit_client/resources/activity.py +++ b/fitbit_client/resources/activity.py @@ -1,9 +1,6 @@ # fitbit_client/resources/activity.py # Standard library imports -from typing import Any -from typing import Dict -from typing import Never from typing import Optional from typing import TYPE_CHECKING from typing import Union diff --git a/fitbit_client/resources/activity_timeseries.py b/fitbit_client/resources/activity_timeseries.py index d7ba673..3e7d667 100644 --- a/fitbit_client/resources/activity_timeseries.py +++ b/fitbit_client/resources/activity_timeseries.py @@ -1,8 +1,6 @@ # fitbit_client/resources/activity_timeseries.py # Standard library imports -from typing import Any -from typing import Dict from typing import cast # Local imports diff --git a/fitbit_client/resources/breathing_rate.py b/fitbit_client/resources/breathing_rate.py index d322ae8..2ebf843 100644 --- a/fitbit_client/resources/breathing_rate.py +++ b/fitbit_client/resources/breathing_rate.py @@ -1,7 +1,6 @@ # fitbit_client/resources/breathing_rate.py # Standard library imports -from typing import Dict from typing import cast # Local imports diff --git a/fitbit_client/resources/cardio_fitness_score.py b/fitbit_client/resources/cardio_fitness_score.py index f5b12d1..ce86816 100644 --- a/fitbit_client/resources/cardio_fitness_score.py +++ b/fitbit_client/resources/cardio_fitness_score.py @@ -1,7 +1,6 @@ # fitbit_client/resources/cardio_fitness_score.py # Standard library imports -from typing import Dict from typing import cast # Local imports diff --git a/fitbit_client/resources/electrocardiogram.py b/fitbit_client/resources/electrocardiogram.py index b33023b..ee14cc4 100644 --- a/fitbit_client/resources/electrocardiogram.py +++ b/fitbit_client/resources/electrocardiogram.py @@ -1,8 +1,6 @@ # fitbit_client/resources/electrocardiogram.py # Standard library imports -from typing import Any -from typing import Dict from typing import Optional from typing import TYPE_CHECKING from typing import Union diff --git a/fitbit_client/resources/intraday.py b/fitbit_client/resources/intraday.py index 1c1e71a..8dc9b68 100644 --- a/fitbit_client/resources/intraday.py +++ b/fitbit_client/resources/intraday.py @@ -1,7 +1,6 @@ # fitbit_client/resources/intraday.py # Standard library imports -from logging import getLogger from typing import Optional from typing import cast diff --git a/fitbit_client/resources/nutrition.py b/fitbit_client/resources/nutrition.py index 7f1af53..0907f0f 100644 --- a/fitbit_client/resources/nutrition.py +++ b/fitbit_client/resources/nutrition.py @@ -4,13 +4,11 @@ from typing import Dict from typing import List from typing import Optional -from typing import Union from typing import cast # Local imports from fitbit_client.exceptions import ClientValidationException from fitbit_client.exceptions import MissingParameterException -from fitbit_client.exceptions import ValidationException from fitbit_client.resources._base import BaseResource from fitbit_client.resources._constants import FoodFormType from fitbit_client.resources._constants import FoodPlanIntensity diff --git a/fitbit_client/resources/sleep.py b/fitbit_client/resources/sleep.py index fada41b..ad6b469 100644 --- a/fitbit_client/resources/sleep.py +++ b/fitbit_client/resources/sleep.py @@ -1,8 +1,6 @@ # fitbit_client/resources/sleep.py # Standard library imports -from typing import Any -from typing import Dict from typing import Optional from typing import TYPE_CHECKING from typing import Union diff --git a/fitbit_client/utils/pagination_validation.py b/fitbit_client/utils/pagination_validation.py index 1df0fe3..132ee45 100644 --- a/fitbit_client/utils/pagination_validation.py +++ b/fitbit_client/utils/pagination_validation.py @@ -4,7 +4,6 @@ from functools import wraps from inspect import signature from typing import Callable -from typing import Optional from typing import ParamSpec from typing import TypeVar from typing import cast diff --git a/tests/fitbit_client/resources/activity/test_get_activity_log_list.py b/tests/fitbit_client/resources/activity/test_get_activity_log_list.py index cfba3e0..3aa7703 100644 --- a/tests/fitbit_client/resources/activity/test_get_activity_log_list.py +++ b/tests/fitbit_client/resources/activity/test_get_activity_log_list.py @@ -4,7 +4,6 @@ # Standard library imports from unittest.mock import Mock -from unittest.mock import call from unittest.mock import patch # Third party imports diff --git a/tests/fitbit_client/resources/nutrition/test_create_food.py b/tests/fitbit_client/resources/nutrition/test_create_food.py index 20ae740..2fa5416 100644 --- a/tests/fitbit_client/resources/nutrition/test_create_food.py +++ b/tests/fitbit_client/resources/nutrition/test_create_food.py @@ -7,7 +7,6 @@ # Local imports from fitbit_client.exceptions import ClientValidationException -from fitbit_client.exceptions import ValidationException from fitbit_client.resources._constants import FoodFormType from fitbit_client.resources._constants import NutritionalValue diff --git a/tests/fitbit_client/resources/nutrition/test_error_handling.py b/tests/fitbit_client/resources/nutrition/test_error_handling.py index d884226..37e720b 100644 --- a/tests/fitbit_client/resources/nutrition/test_error_handling.py +++ b/tests/fitbit_client/resources/nutrition/test_error_handling.py @@ -3,16 +3,12 @@ """Tests for error handling in nutrition endpoints.""" # Standard library imports -from unittest.mock import Mock # Third party imports from pytest import raises # Local imports -from fitbit_client.exceptions import InsufficientPermissionsException from fitbit_client.exceptions import InvalidTokenException -from fitbit_client.exceptions import NotFoundException -from fitbit_client.exceptions import RateLimitExceededException from fitbit_client.exceptions import SystemException from fitbit_client.exceptions import ValidationException from fitbit_client.resources._constants import MealType diff --git a/tests/fitbit_client/resources/sleep/test_get_sleep_log_list.py b/tests/fitbit_client/resources/sleep/test_get_sleep_log_list.py index 914953f..9834cd6 100644 --- a/tests/fitbit_client/resources/sleep/test_get_sleep_log_list.py +++ b/tests/fitbit_client/resources/sleep/test_get_sleep_log_list.py @@ -3,7 +3,6 @@ """Tests for the get_sleep_log_list endpoint.""" # Standard library imports -from unittest.mock import call from unittest.mock import patch # Third party imports diff --git a/tests/fitbit_client/resources/test_base.py b/tests/fitbit_client/resources/test_base.py index 4708224..aee1cd0 100644 --- a/tests/fitbit_client/resources/test_base.py +++ b/tests/fitbit_client/resources/test_base.py @@ -1002,7 +1002,7 @@ def test_rate_limit_headers_logging(base_resource, mock_logger): base_resource.oauth = Mock() base_resource.oauth.request.return_value = mock_response - result = base_resource._make_request("test/endpoint") + base_resource._make_request("test/endpoint") # Verify the debug log contains rate limit information for call in mock_logger.debug.call_args_list: diff --git a/tests/fitbit_client/resources/test_docstrings.py b/tests/fitbit_client/resources/test_docstrings.py index de6872b..5e6aa75 100644 --- a/tests/fitbit_client/resources/test_docstrings.py +++ b/tests/fitbit_client/resources/test_docstrings.py @@ -3,7 +3,6 @@ # Standard library imports from inspect import getdoc from inspect import getmembers -from inspect import isclass from inspect import isfunction from re import search from typing import List diff --git a/tests/fitbit_client/resources/test_pagination.py b/tests/fitbit_client/resources/test_pagination.py index 023203d..8d9da64 100644 --- a/tests/fitbit_client/resources/test_pagination.py +++ b/tests/fitbit_client/resources/test_pagination.py @@ -5,10 +5,7 @@ # Standard library imports import sys import typing -from typing import Any -from typing import Dict from unittest.mock import Mock -from unittest.mock import patch # Third party imports from pytest import fixture @@ -74,7 +71,6 @@ def test_import_with_type_checking(): # Now import the module with TYPE_CHECKING as True # Local imports - from fitbit_client.resources._pagination import PaginatedIterator # This should have imported BaseResource due to TYPE_CHECKING being True assert "fitbit_client.resources._base" in sys.modules diff --git a/tests/fitbit_client/utils/test_curl_debug_mixin.py b/tests/fitbit_client/utils/test_curl_debug_mixin.py index dff9948..7f41d75 100644 --- a/tests/fitbit_client/utils/test_curl_debug_mixin.py +++ b/tests/fitbit_client/utils/test_curl_debug_mixin.py @@ -4,7 +4,6 @@ # Standard library imports from unittest.mock import Mock -from unittest.mock import patch # Third party imports from pytest import fixture diff --git a/tests/fitbit_client/utils/test_date_validation.py b/tests/fitbit_client/utils/test_date_validation.py index 34bee4b..6ef979d 100644 --- a/tests/fitbit_client/utils/test_date_validation.py +++ b/tests/fitbit_client/utils/test_date_validation.py @@ -1,8 +1,6 @@ # tests/fitbit_client/utils/test_date_validation.py # Standard library imports -from typing import Any -from typing import Dict from typing import Optional # Third party imports From 8e5ac977790b9ac522a6ddd33b591d2668ef31a4 Mon Sep 17 00:00:00 2001 From: Jon Stroop Date: Mon, 10 Mar 2025 15:17:05 -0700 Subject: [PATCH 2/2] Update rate limiting documentation to match implementation - Fix incorrect default values in config example - Add comprehensive exponential backoff example - Update log message format to match actual output - Add advanced usage section with datetime-based retry example --- docs/RATE_LIMITING.md | 43 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/docs/RATE_LIMITING.md b/docs/RATE_LIMITING.md index e04205c..6353882 100644 --- a/docs/RATE_LIMITING.md +++ b/docs/RATE_LIMITING.md @@ -48,9 +48,9 @@ client = FitbitClient( redirect_uri="https://localhost:8080", # Rate limiting options (all optional) - max_retries=3, # Maximum retry attempts (default: 5) - retry_after_seconds=10, # Base wait time if headers missing (default: 30) - retry_backoff_factor=1.5 # Multiplier for successive waits (default: 2.0) + max_retries=5, # Maximum retry attempts (default: 3) + retry_after_seconds=60, # Base wait time if headers missing (default: 60) + retry_backoff_factor=1.5 # Multiplier for successive waits (default: 1.5) ) ``` @@ -68,12 +68,13 @@ The client uses the following strategy for retries: retry_time = retry_after_seconds * (retry_backoff_factor ^ retry_count) ``` -With the default settings and no headers: +With example settings of 5 retries and no headers: -- First retry: Wait 30 seconds -- Second retry: Wait 60 seconds (30 * 2.0) -- Third retry: Wait 240 seconds (60 * 2.0²) -- Fourth retry: Wait 240 seconds (240 * 2.0²) +- First retry: Wait 60 seconds (base time) +- Second retry: Wait 90 seconds (60 * 1.5¹) +- Third retry: Wait 135 seconds (60 * 1.5²) +- Fourth retry: Wait 202.5 seconds (60 * 1.5³) +- Fifth retry: Wait 303.75 seconds (60 * 1.5⁴) ## Logging @@ -93,7 +94,7 @@ client = FitbitClient(...) You'll see log messages like: ``` -WARNING:fitbit_client.SleepResource:Rate limit exceeded for get_sleep_log_list to sleep/list.json. [Rate Limit: 0/150, Reset in: 600s] (Will retry after 600 seconds if retries are enabled) +WARNING:fitbit_client.SleepResource:Rate limit exceeded for get_sleep_log_list to sleep/list.json. [Rate Limit: 0/150] Retrying in 600 seconds. (4 retries remaining) ``` ## Handling Unrecoverable Rate Limits @@ -133,3 +134,27 @@ except RateLimitExceededException as e: These can be used to implement more sophisticated retry or backoff strategies in your application. + +## Advanced Usage + +You can implement custom strategies by combining rate limit information with +your own timing logic: + +```python +from datetime import datetime, timedelta +from time import sleep + +try: + client.get_daily_activity_summary(date="today") +except RateLimitExceededException as e: + # Calculate next reset time (typically the top of the next hour) + reset_time = datetime.now() + timedelta(seconds=e.rate_limit_reset) + print(f"Rate limit reached. Pausing until {reset_time.strftime('%H:%M:%S')}") + + # Wait until reset time plus a small buffer + wait_seconds = e.rate_limit_reset + 5 + sleep(wait_seconds) + + # Try again after waiting + client.get_daily_activity_summary(date="today") +```