From af3993a6b8a47335b0b90c9f90be9d9279186c67 Mon Sep 17 00:00:00 2001 From: Jon Stroop Date: Wed, 5 Mar 2025 06:40:12 -0500 Subject: [PATCH 1/2] Update and streamline README --- README.md | 44 ++++++++++++++++---------------------------- TODO.md | 17 +++++++++++++++++ 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 2f8c539..b3099cb 100644 --- a/README.md +++ b/README.md @@ -59,9 +59,7 @@ The response will always be the body of the API response, and is almost always a `Dict`, `List` or `None`. `nutrition.get_activity_tcx` is the exception. It returns XML (as a `str`). -## Authentication Methods - -### 1. Automatic (Recommended) +## Authentication Uses a local callback server to automatically handle the OAuth2 flow: @@ -69,40 +67,26 @@ Uses a local callback server to automatically handle the OAuth2 flow: client = FitbitClient( client_id="YOUR_CLIENT_ID", client_secret="YOUR_CLIENT_SECRET", - redirect_uri="https://localhost:8080", - use_callback_server=True # default is True + redirect_uri="YOUR_REGISTERED_REDIRECT_URI", + token_cache_path="/tmp/fb_tokens.json" # Optional: saves tokens between sessions ) # Will open browser and handle callback automatically client.authenticate() ``` -### 2. Manual URL Copy/Paste +The `token_cache_path` parameter allows you to persist authentication tokens +between sessions. If provided, the client will: -If you prefer not to use a local server: - -```python -client = FitbitClient( - client_id="YOUR_CLIENT_ID", - client_secret="YOUR_CLIENT_SECRET", - redirect_uri="YOUR_REGISTERED_REDIRECT_URI", - token_cache_path="/tmp/fb_tokens.json", - use_callback_server=True -) - -# Will open a browser and start a server to complete the flow (default), or -# prompt you on the command line to copy/paste the callback URL from the -# browser (see `use_callback_server`) -client.authenticate() -``` +1. Load existing tokens from this file if available (avoiding re-authentication) +2. Save new or refreshed tokens to this file automatically +3. Handle token refresh when expired tokens are detected ## Setting Up Your Fitbit App 1. Go to dev.fitbit.com and create a new application 2. Set OAuth 2.0 Application Type to "Personal" -3. Set Callback URL to: - - For automatic method: "https://localhost:8080" - - For manual method: Your preferred redirect URI +3. Set Callback URL to "https://localhost:8080" (or your preferred local URL) 4. Copy your Client ID and Client Secret Additional documentation: @@ -124,9 +108,13 @@ This client does not currently support the and [deletion](https://dev.fitbit.com/build/reference/web-api/subscription/delete-subscription/) of -[webhook subscrptions](https://dev.fitbit.com/build/reference/web-api/developer-guide/using-subscriptions/). -The methods are implemented in comments and _should_ work, but I have not had a -chance to verify a user confirm this. +[webhook subscriptions](https://dev.fitbit.com/build/reference/web-api/developer-guide/using-subscriptions/). +The methods are implemented in comments and should work, but I have not had a +chance to verify them since this requires a publicly accessible server to +receive webhook notifications. + +If you're using this library with subscriptions and would like to help test and +implement this functionality, please open an issue or pull request! ## License diff --git a/TODO.md b/TODO.md index 0cff1f3..46a172a 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,23 @@ ## TODOs: +- Improve README for end users: + + - Add more common use cases examples beyond basic profile retrieval + - Explain token persistence between sessions (DONE) + - Provide overview of available resources/endpoints + - Verify correct callback URI guidance (check if "https://localhost:8080" is + actually the correct/optimal value to recommend) + +- Review and improve all documentation files in docs/ from an end-user + perspective: + + - DEVELOPMENT.md + - LOGGING.md + - NAMING_AND_TYPING.md + - STYLE.md + - VALIDATIONS_AND_EXCEPTIONS.md + - PyPi deployment - For all `create_...`methods, add the ID from the response to logs and maybe From 59f9164fcaaae3d7141e14be43f782300337e3ba Mon Sep 17 00:00:00 2001 From: Jon Stroop Date: Thu, 6 Mar 2025 06:10:18 -0500 Subject: [PATCH 2/2] Improve documentation organization and readability - Split NAMING_AND_TYPING.md into TYPES.md and NAMING.md - Split VALIDATIONS_AND_EXCEPTIONS.md into VALIDATIONS.md and ERROR_HANDLING.md - Update README.md with organized documentation links - Fix intraday data support information in DEVELOPMENT.md - Add information about disabling data logging to LOGGING.md - Update TODO.md to reflect completed documentation work --- README.md | 29 +- TODO.md | 23 +- docs/DEVELOPMENT.md | 34 ++- docs/ERROR_HANDLING.md | 181 ++++++++++++ docs/LOGGING.md | 29 +- docs/NAMING.md | 59 ++++ docs/{NAMING_AND_TYPING.md => TYPES.md} | 104 ++++--- docs/VALIDATIONS.md | 200 +++++++++++++ docs/VALIDATIONS_AND_EXCEPTIONS.md | 367 ------------------------ 9 files changed, 578 insertions(+), 448 deletions(-) create mode 100644 docs/ERROR_HANDLING.md create mode 100644 docs/NAMING.md rename docs/{NAMING_AND_TYPING.md => TYPES.md} (70%) create mode 100644 docs/VALIDATIONS.md delete mode 100644 docs/VALIDATIONS_AND_EXCEPTIONS.md diff --git a/README.md b/README.md index b3099cb..8bee415 100644 --- a/README.md +++ b/README.md @@ -89,17 +89,24 @@ between sessions. If provided, the client will: 3. Set Callback URL to "https://localhost:8080" (or your preferred local URL) 4. Copy your Client ID and Client Secret -Additional documentation: - -- To understand the logging implemementation, see [LOGGING](docs/LOGGING.md) -- To understand validations and the exception hierarchy, see - [VALIDATIONS_AND_EXCEPTIONS](docs/VALIDATIONS_AND_EXCEPTIONS.md) -- It's work checking out - [Fitbit's Best Practices](https://dev.fitbit.com/build/reference/web-api/developer-guide/best-practices/) -- For some general development guidelines, see - [DEVELOPMENT](docs/DEVELOPMENT.md). -- For style guidelines (mostly enforced through varius linters and formatters) - see [STYLE](docs/STYLE.md). +## Additional Documentation + +### For API Library Users + +- [LOGGING.md](docs/LOGGING.md): Understanding the dual-logger system +- [TYPES.md](docs/TYPES.md): JSON type system and method return types +- [NAMING.md](docs/NAMING.md): API method naming conventions +- [VALIDATIONS.md](docs/VALIDATIONS.md): Input parameter validation +- [ERROR_HANDLING.md](docs/ERROR_HANDLING.md): Exception hierarchy and handling + +It's also worth reviewing +[Fitbit's Best Practices](https://dev.fitbit.com/build/reference/web-api/developer-guide/best-practices/) +for API usage. + +### Project Best Practices + +- [DEVELOPMENT.md](docs/DEVELOPMENT.md): Development environment and guidelines +- [STYLE.md](docs/STYLE.md): Code style and formatting standards ## Important Note - Subscription Support diff --git a/TODO.md b/TODO.md index 46a172a..8295439 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,9 @@ ## TODOs: +- Create and Test that all methods have an alias in `Client` and that the + signatures match + - Improve README for end users: - Add more common use cases examples beyond basic profile retrieval @@ -10,14 +13,15 @@ - Verify correct callback URI guidance (check if "https://localhost:8080" is actually the correct/optimal value to recommend) -- Review and improve all documentation files in docs/ from an end-user - perspective: +- ✅ Review and improve all documentation files in docs/ from an end-user + perspective - - DEVELOPMENT.md - - LOGGING.md - - NAMING_AND_TYPING.md - - STYLE.md - - VALIDATIONS_AND_EXCEPTIONS.md + - ✅ Split NAMING_AND_TYPING.md into TYPES.md and NAMING.md + - ✅ Split VALIDATIONS_AND_EXCEPTIONS.md into VALIDATIONS.md and + ERROR_HANDLING.md + - ✅ Update cross-references between documentation files + - ✅ Fix intraday data support information in DEVELOPMENT.md + - ✅ Add information about disabling data logging to LOGGING.md - PyPi deployment @@ -34,11 +38,6 @@ - Rename to `_base`? Files it first, makes it clearer that everything in it is private -- client.py: - - - Creat and Test that all methods have an alias in `Client` and that the - signatures match - - CI: * Read and implement: diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 367c392..6d01e06 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -339,18 +339,32 @@ TODO - Use issue templates when reporting bugs - Include Python version and environment details in bug reports -## Scope and Limitations - Intraday Data Support +## Intraday Data Support -This client explicitly does not implement intraday data endpoints (detailed -heart rate, steps, etc). These endpoints: +This client implements intraday data endpoints (detailed heart rate, steps, etc) +through the `IntradayResource` class. These endpoints: - Require special access from Fitbit (typically limited to research applications) - Have different rate limits than standard endpoints -- Need additional OAuth2 scopes -- Often require institutional review board (IRB) approval - -If you need intraday data access: - -1. Apply through Fitbit's developer portal -2. Pull requests welcome! +- Need additional OAuth2 scopes (specifically the 'activity' and 'heartrate' + scopes) +- Often require institutional review board (IRB) approval for research + applications + +To use intraday data: + +1. Apply for intraday access through the + [Fitbit developer portal](https://dev.fitbit.com/) +2. Ensure your application requests the appropriate scopes +3. Use the intraday methods with appropriate detail level parameters: + ```python + client.intraday.get_heartrate_intraday_by_date( + date="today", + detail_level="1min" # or "1sec" depending on your access level + ) + ``` + +See the +[Intraday API documentation](https://dev.fitbit.com/build/reference/web-api/intraday/) +for more details on available endpoints and access requirements. diff --git a/docs/ERROR_HANDLING.md b/docs/ERROR_HANDLING.md new file mode 100644 index 0000000..eda1cb6 --- /dev/null +++ b/docs/ERROR_HANDLING.md @@ -0,0 +1,181 @@ +# Exception Handling + +The library implements a comprehensive exception system to help you handle +errors effectively. Understanding these exceptions will help you write more +robust code. + +## Exception Hierarchy + +``` +Exception +├── ValueError +│ └── ClientValidationException # Superclass for validations that take place before +│ │ # making a request +│ ├── InvalidDateException # Raised when a date string is not in the correct +│ │ # format or not a valid calendar date +│ ├── InvalidDateRangeException # Raised when a date range is invalid (e.g., end is +│ │ # before start, exceeds max days) +│ ├── PaginationException # Raised when pagination parameters are invalid +│ ├── IntradayValidationException # Raised when intraday request parameters are invalid +│ ├── ParameterValidationException # Raised when a parameter value is invalid +│ │ # (e.g., negative when positive required) +│ └── MissingParameterException # Raised when required parameters are missing or +│ # parameter combinations are invalid +│ +└── FitbitAPIException # Base exception for all Fitbit API errors + │ + ├── OAuthException # Superclass for all authentication flow exceptions + │ ├── ExpiredTokenException # Raised when the OAuth token has expired + │ ├── InvalidGrantException # Raised when the grant_type value is invalid + │ ├── InvalidTokenException # Raised when the OAuth token is invalid + │ └── InvalidClientException # Raised when the client_id is invalid + │ + └── RequestException # Superclass for all API request exceptions + ├── InvalidRequestException # Raised when the request syntax is invalid + ├── AuthorizationException # Raised when there are authorization-related errors + ├── InsufficientPermissionsException # Raised when the application has insufficient permissions + ├── InsufficientScopeException # Raised when the application is missing a required scope + ├── NotFoundException # Raised when the requested resource does not exist + ├── RateLimitExceededException # Raised when the application hits rate limiting quotas + ├── SystemException # Raised when there is a system-level failure + └── ValidationException # Raised when a request parameter is invalid or missing +``` + +## Client Validation Exceptions + +Client validation exceptions (`ClientValidationException` and its subclasses) +are raised *before* any API call is made: + +1. They reflect problems with your input parameters that can be detected locally +2. No network requests have been initiated when these exceptions occur +3. They help you fix issues before consuming API rate limits + +```python +from fitbit_client.exceptions import InvalidDateException, InvalidDateRangeException + +try: + client.sleep.get_sleep_log_by_date_range( + start_date="2024-01-01", + end_date="2023-12-31" # End date before start date + ) +except InvalidDateRangeException as e: + print(f"Date range error: {e.message}") + print(f"Start date: {e.start_date}, End date: {e.end_date}") + print(f"Resource: {e.resource_name}, Max days: {e.max_days}") +``` + +### Common Client Validation Exceptions + +- **InvalidDateException**: Raised when a date string is not valid +- **InvalidDateRangeException**: Raised when a date range is invalid +- **ParameterValidationException**: Raised when a parameter value is invalid +- **MissingParameterException**: Raised when required parameters are missing +- **PaginationException**: Raised when pagination parameters are invalid +- **IntradayValidationException**: Raised when intraday request parameters are + invalid + +## API Exceptions + +API exceptions (`FitbitAPIException` and its subclasses) are raised in response +to errors returned by the Fitbit API: + +```python +from fitbit_client.exceptions import AuthorizationException, RateLimitExceededException + +try: + client.activity.get_lifetime_stats() +except AuthorizationException as e: + print(f"Auth error ({e.status_code}): {e.message}") + # Handle authentication error (e.g., refresh token, prompt for re-auth) +except RateLimitExceededException as e: + retry_after = int(e.headers.get("Retry-After", 60)) + print(f"Rate limit exceeded. Retry after {retry_after} seconds") + # Implement backoff strategy +``` + +### Common API Exceptions + +- **AuthorizationException**: Authentication or authorization issues +- **InvalidRequestException**: Invalid request syntax or parameters +- **RateLimitExceededException**: API rate limits exceeded +- **NotFoundException**: Requested resource doesn't exist +- **SystemException**: Fitbit API server-side errors + +## Exception Properties + +### Client Validation Exceptions + +All client validation exceptions have these properties: + +- `message`: Human-readable error description +- `field_name`: Name of the invalid field (if applicable) + +Specific validation exception types add additional properties: + +- **InvalidDateException**: `date_str` (the invalid date string) +- **InvalidDateRangeException**: `start_date`, `end_date`, `max_days`, + `resource_name` +- **IntradayValidationException**: `allowed_values`, `resource_name` + +### API Exceptions + +All API exceptions have these properties: + +- `message`: Human-readable error description +- `status_code`: HTTP status code (if applicable) +- `error_type`: Type of error from the API +- `field_name`: Name of the invalid field (for validation errors) +- `headers`: Response headers (useful for rate limiting info) + +## Usage Patterns + +### Catching Specific Exceptions + +Target specific exceptions for tailored error handling: + +```python +try: + client.activity.create_activity_goals( + period=ActivityGoalPeriod.DAILY, + type=ActivityGoalType.STEPS, + value=-1000 + ) +except ParameterValidationException as e: + print(f"Invalid value for {e.field_name}: {e.message}") +except AuthorizationException as e: + print(f"Authorization error: {e.message}") +except RateLimitExceededException as e: + print(f"Rate limit error: {e.message}") +``` + +### Catching Base Exception Classes + +Catch base classes to handle related exceptions together: + +```python +try: + client.activity.get_daily_activity_summary("today") +except ClientValidationException as e: + print(f"Invalid input: {e.message}") # Catches all input validation errors +except OAuthException as e: + print(f"OAuth error: {e.message}") # Catches all OAuth-related errors +except FitbitAPIException as e: + print(f"API error: {e.message}") # Catches all other API errors +``` + +## Debugging APIs + +Every method accepts a `debug` parameter that prints the equivalent cURL +command: + +```python +client.activity.get_daily_activity_summary( + date="today", + debug=True +) +# Prints: +# curl -X GET -H "Authorization: Bearer " ... +``` + +This helps troubleshoot API interactions by showing the exact request being +made. diff --git a/docs/LOGGING.md b/docs/LOGGING.md index 365f820..92d900a 100644 --- a/docs/LOGGING.md +++ b/docs/LOGGING.md @@ -79,10 +79,10 @@ The data logger uses INFO level for all entries, with a structured JSON format: ``` As you can see, this is really just summary of the response body that makes it -easy to get back in information you may not have captured in a one-off script. +easy to get back information you may not have captured in a one-off script. -Note that the log will not be valid JSON file, but each line will be a valid -object and it is be trivial to read it in at as `List[Dict[str, Any]]`. +Note that the log will not be a valid JSON file, but each line will be a valid +JSON object and it is trivial to read it in as `List[Dict[str, Any]]`. ## Important Fields @@ -129,6 +129,24 @@ data_logger.setLevel("INFO") data_logger.propagate = False # Prevent duplicate logging ``` +### Disabling Data Logging + +If you don't need the data logging feature, you can disable it completely by +setting the level to CRITICAL: + +```python +data_logger = getLogger("fitbit_client.data") +data_logger.setLevel("CRITICAL") # Effectively disables data logging +``` + +You can also disable it by not adding any handlers to the data logger: + +```python +data_logger = getLogger("fitbit_client.data") +data_logger.propagate = False # Ensures logs don't propagate to parent loggers +# No handlers added = no logging output +``` + ## Error Logging The client automatically logs all API errors with rich context including: @@ -138,3 +156,8 @@ The client automatically logs all API errors with rich context including: - Affected resource/endpoint - Field-specific validation errors - Raw error response when available + +## Cross-References + +For more information on error handling, see +[ERROR_HANDLING.md](ERROR_HANDLING.md). diff --git a/docs/NAMING.md b/docs/NAMING.md new file mode 100644 index 0000000..9ebc51d --- /dev/null +++ b/docs/NAMING.md @@ -0,0 +1,59 @@ +# API Method Naming Conventions + +## Naming Principles + +The method names in this library are designed to align with the official Fitbit +Web API Documentation. When there are inconsistencies in the official +documentation, we follow these principles: + +1. The URL slug in the documentation is the primary reference +2. Method names always use underscores (snake_case), not camelCase +3. The HTTP verb is reflected in the method name prefix: + - `get_`: For HTTP GET operations + - `create_`: For HTTP POST operations that create new resources + - `update_`: For HTTP PUT operations + - `delete_`: For HTTP DELETE operations + - `add_`: Only used in specific contexts where "add" is more intuitive than + "create" + +## API Documentation Alignment + +For any discrepancies between different parts of the official documentation, we +prioritize the URL slug. For example, if the documentation page title says "Get +Time Series by Date" but the URL is ".../get-azm-timeseries-by-date/", our +method will be named `get_azm_timeseries_by_date()`. + +This approach ensures consistent naming throughout the library and makes methods +easier to find based on the official documentation. + +## Inconsistencies in the API + +The Fitbit API contains several inconsistencies, which our method names +necessarily reflect: + +- `create_activity_goals` creates only one goal at a time +- `add_favorite_foods` adds one food at a time, while all other creation methods + use "create" prefix +- Some pluralized methods return lists, while others return dictionaries + containing lists + +## Method Aliases + +For user convenience, some methods have aliases: + +- `create_activity_goal` -> `create_activity_goals` +- `add_favorite_food` -> `add_favorite_foods` +- `create_favorite_food` -> `add_favorite_foods` +- `delete_favorite_food` -> `delete_favorite_foods` +- `create_sleep_goal` -> `create_sleep_goals` +- `get_sleep_goal` -> `get_sleep_goals` + +These aliases help accommodate different expectations and ensure backward +compatibility. + +## Resource Structure + +The client organizes API endpoints into logical resource classes, each +representing a different section of the Fitbit API. For a complete list of all +methods and their return types, see +[TYPES.md](TYPES.md#method-return-types-reference). diff --git a/docs/NAMING_AND_TYPING.md b/docs/TYPES.md similarity index 70% rename from docs/NAMING_AND_TYPING.md rename to docs/TYPES.md index 1ec4604..e7b30ec 100644 --- a/docs/NAMING_AND_TYPING.md +++ b/docs/TYPES.md @@ -1,29 +1,53 @@ -# Method Naming and Typing - -Typing and JSON is awkward, but it's useful to know what you should expect to -get back when you call a method. For this reason, all of the resource methods -(i.e. API endpoints) return either `JSONDict`, `JSONList`, `None`. In the case -of the latter two, you're on your own once you're inside the structure, but -knowing the wrapper is at least a good start at sanity. - -Note that we deviate from the native API a little bit in that the content-type -and response body of `delete_*` methods is not consistent: some return an empty -body, some return `null`, and at least one returns `{}`. Here's the deal: if the -reponse status from a API call is `204`, you will get `None`. This is -[in line with the documentation](https://dev.fitbit.com/build/reference/web-api/troubleshooting-guide/error-messages/#204-no-content). - -An area where it's tempting to deviate, but we don't, is in data structures in -the body of the responses. To start, the interns who developed the -[Web API](https://dev.fitbit.com/build/reference/web-api/) were not very -consistent with naming and typing of the API endpoints or the responses. Just a -few examples: - -- `create_activity_goals` only allows you to create one goal at a time -- `add_favorite_foods` adds one food at a time, and "add" is only used here. - It's "create" everywhere else. -- Method names that suggest they would return a list usually don't. They use - this structure (for example, from - `activity_timeseries#get_time_series_by_date`) +# JSON Type System + +## Overview + +This library uses a type system to help you understand what to expect from API +responses. All resource methods (API endpoints) return one of three types: + +- `JSONDict`: A dictionary containing JSON data +- `JSONList`: A list containing JSON data +- `None`: For operations that don't return data (typically delete operations) + +While Python's dynamic typing doesn't enforce these types at runtime, they +provide valuable documentation and enable IDE autocompletion. + +## Understanding Response Types + +### JSONDict and JSONList + +The base types represent the outermost wrapper of API responses: + +```python +# These are the actual definitions from utils/types.py +JSONDict = Dict[str, JSONType] # A dictionary with string keys and JSON values +JSONList = List[JSONType] # A list of JSON values +``` + +Where `JSONType` is a recursive type that can be any valid JSON value: + +```python +JSONType = Union[str, int, float, bool, None, Dict[str, Any], List[Any]] +``` + +This typing helps you know the outer structure of the response but doesn't +specify the inner details. + +### Empty and Special Responses + +The library standardizes response handling in these cases: + +- HTTP 204 responses (No Content): Return `None` +- DELETE operations: Return `None` (regardless of how the API responds) +- Special formats like TCX/XML: Return as raw strings (not JSON) + +## Response Inconsistencies + +The Fitbit API has some inconsistencies in response structures. Some methods +that suggest they would return lists (by their plural names) actually return +dictionaries with embedded lists. + +For example, `get_activity_timeseries_by_date()` returns: ```json { @@ -40,15 +64,15 @@ few examples: } ``` -This would be a lovely and reliable convention! Except that: +This is typed as a `JSONDict`, not a `JSONList`, despite containing a list of +items. + +In contrast, these methods do return direct lists (typed as `JSONList`): ``` get_favorite_activities get_frequent_activities -get_recent_activity_types -get_favorite_activities -get_frequent_activities -get_recent_activity_types +get_recent_activity_types get_devices get_food_locales get_food_units @@ -57,19 +81,9 @@ get_recent_foods get_spo2_summary_by_interval ``` -All return lists. If there is a rhyme or reason for this, I've not found it yet. - -## Naming - -Methods are named exactly as they appear in the -[Web API Documentation](https://dev.fitbit.com/build/reference/web-api/). When -there are inconsistencies (frequently) the documentation;s URL slug is the -deciding factor. For example, for "Get AZM Time Series by Date" -https://dev.fitbit.com/build/reference/web-api/active-zone-minutes-timeseries/get-azm-timeseries-by-date/, -(which is it--"Time Series" or "timeseries"?) the method in our code will be -`get_azm_timeseries_by_date()`. +## Method Return Types Reference -## Method Return Types +Below is a comprehensive list of all method return types by resource class: ### ActiveZoneMinutesResource @@ -86,8 +100,8 @@ https://dev.fitbit.com/build/reference/web-api/active-zone-minutes-timeseries/ge - `create_activity_goals -> JSONDict` - `create_activity_goal -> JSONDict` (alias for create_activity_goals) - `create_activity_log -> JSONDict` -- `create_favorite_activity -> Dict[Never, Never]` ?? -- `delete_activity_log -> Dict[Never, Never]` ?? +- `create_favorite_activity -> Dict[Never, Never]` +- `delete_activity_log -> Dict[Never, Never]` - `get_activity_log_list -> JSONDict` - `delete_favorite_activity -> None` - `get_activity_goals -> JSONDict` diff --git a/docs/VALIDATIONS.md b/docs/VALIDATIONS.md new file mode 100644 index 0000000..15feb94 --- /dev/null +++ b/docs/VALIDATIONS.md @@ -0,0 +1,200 @@ +# Input Validation + +The library performs thorough validation of input parameters **before making any +API requests**. This approach: + +- Preserves your API rate limits by catching errors locally +- Provides more specific and helpful error messages +- Simplifies debugging by separating client-side validation from API issues + +## Date Formats + +All dates must be in one of these formats: + +- YYYY-MM-DD (e.g., "2024-02-20") +- "today" (special keyword for current date) + +```python +# Valid dates +client.activity.get_daily_activity_summary("2024-02-20") +client.activity.get_daily_activity_summary("today") + +# Invalid - will raise InvalidDateException +try: + client.activity.get_daily_activity_summary("02-20-2024") +except InvalidDateException as e: + print(e.message) + # Output: Invalid date format. Use YYYY-MM-DD or 'today' +``` + +## Date Ranges + +When using endpoints that accept date ranges: + +- `start_date` must be before or equal to `end_date` +- Maximum range varies by endpoint: + - Body weight logs: 31 days + - Sleep logs: 100 days + - Activity data: 31 days + - General data: 1095 days (approximately 3 years) + +```python +# Valid range within limits +client.sleep.get_sleep_log_by_date_range( + start_date="2024-01-01", + end_date="2024-01-31" +) + +# Invalid - exceeds sleep data 100 day limit +try: + client.sleep.get_sleep_log_by_date_range( + start_date="2024-01-01", + end_date="2024-12-31" + ) +except InvalidDateRangeException as e: + print(e.message) + # Output: Date range cannot exceed 100 days for sleep time series +``` + +## Enumerated Values + +The library provides enums for many parameters to ensure valid values: + +```python +from fitbit_client.resources.constants import Period, ActivityGoalType + +# Valid - using provided enum +client.activity.get_activity_timeseries_by_date( + resource_path=ActivityTimeSeriesPath.STEPS, + date="today", + period=Period.ONE_WEEK +) + +# Invalid - will raise ValidationException +try: + client.activity.get_activity_timeseries_by_date( + resource_path=ActivityTimeSeriesPath.STEPS, + date="today", + period="invalid_period" + ) +except ValidationException as e: + print(e.message) + # Output: Invalid period value. Use one of: 1d, 7d, 30d, 1w, 1m, 3m, 6m, 1y, max +``` + +## Required Parameters + +Some methods require specific parameter combinations: + +```python +# Valid - using food_id +client.nutrition.create_food_log( + date="2024-02-20", + meal_type_id=MealType.BREAKFAST, + unit_id=1, + amount=1.0, + food_id=12345 # Option 1 +) + +# Valid - using food_name and calories +client.nutrition.create_food_log( + date="2024-02-20", + meal_type_id=MealType.BREAKFAST, + unit_id=1, + amount=1.0, + food_name="My Custom Food", # Option 2 + calories=200 # Option 2 +) + +# Invalid - missing both food_id and (food_name, calories) +try: + client.nutrition.create_food_log( + date="2024-02-20", + meal_type_id=MealType.BREAKFAST, + unit_id=1, + amount=1.0 + ) +except ValidationException as e: + print(e.message) + # Output: Must provide either food_id or (food_name and calories) +``` + +## Numeric Limits + +Many endpoints enforce numeric limits: + +```python +# Invalid - Cannot request more than 100 records +try: + client.get_activity_log_list(limit=200) +except ValidationException as e: + print(e.message) + # Output: Maximum limit is 100 records + +# Invalid - Goal value must be positive +try: + client.activity.create_activity_goals( + period=ActivityGoalPeriod.DAILY, + type=ActivityGoalType.STEPS, + value=-1000 + ) +except ValidationException as e: + print(e.message) + # Output: Goal value must be positive +``` + +## Pagination Parameters + +When using endpoints with pagination: + +- Only one of `offset`/`limit` or `before_date`/`after_date`/`sort` can be used +- `sort` must be consistent with the date parameter used (ascending with + `after_date`, descending with `before_date`) +- Maximum records per request is typically limited (often 100) + +```python +# Valid - using offset/limit pagination +client.activity.get_activity_log_list(offset=0, limit=10) + +# Valid - using date-based pagination +client.activity.get_activity_log_list( + after_date="2024-01-01", + sort="asc" +) + +# Invalid - mixing pagination methods +try: + client.activity.get_activity_log_list( + offset=0, + limit=10, + after_date="2024-01-01" + ) +except PaginationException as e: + print(e.message) + # Output: Cannot mix offset/limit with date-based pagination +``` + +## Custom and Combined Validation + +Some endpoints have custom validation requirements: + +```python +# Custom validation for food log creation +try: + client.nutrition.create_food_log( + date="2024-02-20", + meal_type_id=MealType.BREAKFAST, + food_name="Test Food", + # Missing calories! + unit_id=1, + amount=1.0 + ) +except MissingParameterException as e: + print(e.message) + # Output: When using food_name, calories is required +``` + +## Validation Implementation + +For a complete understanding of the exception system that powers these +validations, see [ERROR_HANDLING.md](ERROR_HANDLING.md). diff --git a/docs/VALIDATIONS_AND_EXCEPTIONS.md b/docs/VALIDATIONS_AND_EXCEPTIONS.md deleted file mode 100644 index 76752d1..0000000 --- a/docs/VALIDATIONS_AND_EXCEPTIONS.md +++ /dev/null @@ -1,367 +0,0 @@ -# Input Validation and Error Handling - -Many method parameter arguments are validated **before making any API -requests**. The aim is to encapsulate the HTTP API as much as possible and raise -more helpful exceptions before a bad request is executed. This approach: - -- Preserves your API rate limits by catching errors locally -- Provides more specific and helpful error messages -- Simplifies debugging by clearly separating client-side validation issues from - API response issues - -Understanding these validations and the exceptions that are raised by them (and -elsewhere) will help you use this library correctly and efficiently. - -## Input Validation - -### Date Formats - -All dates must be in one of these formats: - -- YYYY-MM-DD (e.g., "2024-02-20") -- "today" (special keyword for current date) - -Example: - -```python -# Valid dates -client.activity.get_daily_activity_summary("2024-02-20") -client.activity.get_daily_activity_summary("today") - -# Invalid - will raise InvalidDateException -try: - client.activity.get_daily_activity_summary("02-20-2024") -except InvalidDateException as e: - print(e.message) - # Output: Invalid date format. Use YYYY-MM-DD or 'today' -``` - -### Date Ranges - -When using endpoints that accept date ranges: - -- start_date must be before or equal to end_date -- Maximum range varies by endpoint: - - Body weight logs: 31 days - - Sleep logs: 100 days - - Activity data: 31 days - - General data: 1095 days (approximately 3 years) - -Example: - -```python -# Valid range within limits -client.sleep.get_sleep_log_by_date_range( - start_date="2024-01-01", - end_date="2024-01-31" -) - -# Invalid - exceeds sleep data 100 day limit -try: - client.sleep.get_sleep_log_by_date_range( - start_date="2024-01-01", - end_date="2024-12-31" - ) -except InvalidDateRangeException as e: - print(e.message) - # Output: Date range cannot exceed 100 days for sleep time series -``` - -### Enumerated Values - -The library provides enums for many parameters. Using these enums ensures valid -values: - -```python -from fitbit_client.resources.constants import Period, ActivityGoalType - -# Valid - using provided enum -client.activity.get_activity_timeseries_by_date( - resource_path=ActivityTimeSeriesPath.STEPS, - date="today", - period=Period.ONE_WEEK -) -``` - -### Required Parameters - -Some methods require specific parameter combinations that can be tricky to get -right. For example, creating a food log requires either a `food_id`, or -`food_name` AND `calories`. - -```python -# Valid - using food_id -client.nutrition.create_food_log( - date="2024-02-20", - meal_type_id=MealType.BREAKFAST, - unit_id=1, - amount=1.0, - food_id=12345 # Option 1 -) - -# Valid - using food_name and calories -client.nutrition.create_food_log( - date="2024-02-20", - meal_type_id=MealType.BREAKFAST, - unit_id=1, - amount=1.0, - food_name="My Custom Food", # Option 2 - calories=200 # Option 2 -) - -# Invalid - missing both food_id and (food_name, calories) -try: - client.nutrition.create_food_log( - date="2024-02-20", - meal_type_id=MealType.BREAKFAST, - unit_id=1, - amount=1.0 - ) -except ValidationException as e: - print(e.message) - # Output: Must provide either food_id or (food_name and calories) - -# Invalid - only provided food_name without calories -try: - client.nutrition.create_food_log( - date="2024-02-20", - meal_type_id=MealType.BREAKFAST, - unit_id=1, - amount=1.0, - food_name="My Custom Food" - ) -except ValidationException as e: - print(e.message) - # Output: Must provide either food_id or (food_name and calories) -``` - -### Numeric Limits - -Many endpoints enforce numeric limits: - -```python -# Invalid - Cannot request more than 100 records -try: - client.get_activity_log_list(limit=200) -except ValidationException as e: - print(e.message) - # Output: Maximum limit is 100 records - -# Invalid - Goal value must be positive -try: - client.activity.create_activity_goals( - period=ActivityGoalPeriod.DAILY, - type=ActivityGoalType.STEPS, - value=-1000 - ) -except ValidationException as e: - print(e.message) - # Output: Goal value must be positive - -# Invalid - Duration must be positive -try: - client.activity.create_activity_log( - activity_id=12345, - duration_millis=-60000, # -1 minute - start_time="12:00", - date="2024-02-20" - ) -except ValidationException as e: - print(e.message) - # Output: Duration must be positive -``` - -## Exception Handling - -There are many custom exceptions. When validation fails or other errors occur, -the library raises specific exceptions that help identify the problem. - -### Using Custom Validation Exceptions - -Client validation exceptions (`ClientValidationException` and its subclasses) -are raised *before* any API call is made. This means: - -1. They reflect problems with your input parameters that can be detected locally -2. No network requests have been initiated when these exceptions occur -3. They help you fix issues before consuming API rate limits - -This is in contrast to API exceptions (`FitbitAPIException` and its subclasses), -which are raised in response to errors returned by the Fitbit API after a -network request has been made. - -When using this library, you'll want to catch the specific exception types for -proper error handling: - -```python -from fitbit_client.exceptions import ParameterValidationException, MissingParameterException - -try: - # When parameters might be missing - client.nutrition.create_food_goal(calories=None, intensity=None) -except MissingParameterException as e: - print(f"Missing parameter: {e.message}") - -try: - # When parameters might be invalid - client.sleep.create_sleep_goals(min_duration=-10) -except ParameterValidationException as e: - print(f"Invalid parameter value for {e.field_name}: {e.message}") -``` - -You can also catch the base class for all client validation exceptions: - -```python -from fitbit_client.exceptions import ClientValidationException - -try: - client.activity.create_activity_log(duration_millis=-100, start_time="12:00", date="2024-02-20") -except ClientValidationException as e: - print(f"Validation error: {e.message}") -``` - -### ValidationException - -Raised when input parameters do not meet requirements: - -```python -try: - # Value must be positive - client.activity.create_activity_goal( - period=ActivityGoalPeriod.DAILY, - type=ActivityGoalType.STEPS, - value=-100 - ) -except ValidationException as e: - print(f"Field '{e.field_name}' invalid: {e.message}") - # Output: Field 'value' invalid: Goal value must be positive -``` - -### InvalidDateException - -Raised when a date format is invalid: - -```python -try: - client.activity.get_daily_activity_summary("01-01-2024") -except InvalidDateException as e: - print(e.message) - # Output: Invalid date format. Use YYYY-MM-DD or 'today' -``` - -### InvalidDateRangeException - -Raised when date ranges are invalid or exceed limits: - -```python -try: - client.sleep.get_sleep_log_by_date_range( - start_date="2024-01-01", - end_date="2023-12-31" # End date before start date - ) -except InvalidDateRangeException as e: - print(e.message) -``` - -### AuthorizationException - -Raised for authentication or authorization issues: - -```python -try: - client.activity.get_lifetime_stats() -except AuthorizationException as e: - print(f"Auth error ({e.status_code}): {e.message}") -``` - -### RateLimitExceededException - -Raised when you've exceeded Fitbit's API rate limits: - -```python -try: - client.activity.get_daily_activity_summary("today") -except RateLimitExceededException as e: - print(f"Rate limit exceeded: {e.message}") - # Implement appropriate backoff strategy -``` - -### Exception Properties - -API exceptions (`FitbitAPIException` and its subclasses) provide these -properties: - -- `message`: Human-readable error description -- `status_code`: HTTP status code (if applicable) -- `error_type`: Type of error from the API -- `field_name`: Name of the invalid field (for validation errors) - -Validation exceptions (`ClientValidationException` and its subclasses) provide: - -- `message`: Human-readable error description -- `field_name`: Name of the invalid field (for validation errors) - -Specific validation exception subclasses provide additional properties: - -- `InvalidDateException`: Adds `date_str` property with the invalid date string -- `InvalidDateRangeException`: Adds `start_date`, `end_date`, `max_days`, and - `resource_name` properties -- `IntradayValidationException`: Adds `allowed_values` and `resource_name` - properties -- `ParameterValidationException`: Used for invalid parameter values (e.g., - negative where positive is required) -- `MissingParameterException`: Used when required parameters are missing or - parameter combinations are invalid - -### Exception Hierarchy: - -``` -Exception -├── ValueError -│ └── ClientValidationException # Superclass for validations that take place before -│ │ # making a request -│ ├── InvalidDateException # Raised when a date string is not in the correct -│ │ # format or not a valid calendar date -│ ├── InvalidDateRangeException # Raised when a date range is invalid (e.g., end is -│ │ # before start, exceeds max days) -│ ├── PaginationException # Raised when pagination parameters are invalid -│ ├── IntradayValidationException # Raised when intraday request parameters are invalid -│ ├── ParameterValidationException # Raised when a parameter value is invalid -│ │ # (e.g., negative when positive required) -│ └── MissingParameterException # Raised when required parameters are missing or -│ # parameter combinations are invalid -│ -└── FitbitAPIException # Base exception for all Fitbit API errors - │ - ├── OAuthException # Superclass for all authentication flow exceptions - │ ├── ExpiredTokenException # Raised when the OAuth token has expired - │ ├── InvalidGrantException # Raised when the grant_type value is invalid - │ ├── InvalidTokenException # Raised when the OAuth token is invalid - │ └── InvalidClientException # Raised when the client_id is invalid - │ - └── RequestException # Superclass for all API request exceptions - ├── InvalidRequestException # Raised when the request syntax is invalid - ├── AuthorizationException # Raised when there are authorization-related errors - ├── InsufficientPermissionsException # Raised when the application has insufficient permissions - ├── InsufficientScopeException # Raised when the application is missing a required scope - ├── NotFoundException # Raised when the requested resource does not exist - ├── RateLimitExceededException # Raised when the application hits rate limiting quotas - ├── SystemException # Raised when there is a system-level failure - └── ValidationException # Raised when a request parameter is invalid or missing -``` - -## Debugging - -Every method accepts a `debug` parameter that prints the equivalent cURL -command: - -```python -client.activity.get_daily_activity_summary( - date="today", - debug=True -) -# Prints: -# curl -X GET -H "Authorization: Bearer " ... -``` - -This can help troubleshoot API interactions by showing the exact request being -made.