Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ 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
- [LINTING.md](docs/LINTING.md): Code style and linting configuration

## Important Note - Subscription Support

Expand Down
5 changes: 2 additions & 3 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
- [Response Mocking](#response-mocking)
- [OAuth Callback Implementation](#oauth-callback-implementation)
- [Implementation Flow](#implementation-flow)
- [Security Notes](#security-notes)
- [Git Workflow](#git-workflow)
- [Release Process](#release-process)
- [Getting Help](#getting-help)
Expand All @@ -45,8 +44,8 @@
1. Clone the repository:

```bash
git clone https://github.com/yourusername/fitbit-client.git
cd fitbit-client
git clone https://github.com/jpstroop/fitbit-client-python.git
cd fitbit-client-python
```

2. Install asdf plugins and required versions:
Expand Down
89 changes: 89 additions & 0 deletions docs/ERROR_HANDLING.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,95 @@ except FitbitAPIException as e:
print(f"API error: {e.message}") # Catches all other API errors
```

### Handling OAuth-Specific Exceptions

OAuth errors require special handling. Here's how to handle different OAuth
exception subtypes:

```python
from fitbit_client.exceptions import ExpiredTokenException, InvalidTokenException
from fitbit_client.exceptions import InvalidClientException, InvalidGrantException

try:
# Make an API call that requires authentication
client.get_profile()
except ExpiredTokenException as e:
# Token has expired but couldn't be auto-refreshed
print(f"Token expired: {e.message}")
# Attempt to re-authenticate
client.authenticate()
except InvalidTokenException as e:
# Token is invalid (e.g., revoked by user)
print(f"Token invalid: {e.message}")
# Re-authenticate from scratch
client.authenticate()
except InvalidClientException as e:
# Client credentials are incorrect
print(f"Client credentials error: {e.message}")
# Check client_id and client_secret
except InvalidGrantException as e:
# Refresh token is invalid
print(f"Invalid refresh token: {e.message}")
# Re-authenticate to get a new refresh token
client.authenticate()
```

## Token Refresh Strategies

The client automatically handles token refresh when tokens expire. However, you
may want to implement custom token refresh strategies for your application.

### Automatic Token Refresh

By default, the client refreshes tokens automatically:

1. When initializing the client with cached tokens
2. During API calls when a token expires
3. When explicitly calling a method that requires authentication

```python
# Tokens are automatically refreshed
client = FitbitClient(
client_id="YOUR_CLIENT_ID",
client_secret="YOUR_CLIENT_SECRET",
redirect_uri="https://localhost:8080",
token_cache_path="/path/to/tokens.json" # Enable persistent token caching
)

# If cached tokens exist and are valid or can be refreshed, no browser prompt occurs
client.authenticate()

# If the token expires during this call, it will be refreshed automatically
client.get_profile()
```

### Handling Failed Token Refresh

When automatic token refresh fails, the client raises an appropriate OAuth
exception. Here's a complete error handling pattern:

```python
from fitbit_client.exceptions import OAuthException, ExpiredTokenException

try:
result = client.get_profile()
# Process result normally
except ExpiredTokenException:
# Token expired and auto-refresh failed
try:
# Try to authenticate again
client.authenticate()
# Retry the original request
result = client.get_profile()
except OAuthException as oauth_error:
# Handle authentication failure (e.g., user closed browser window)
print(f"Authentication failed: {oauth_error.message}")
# Log the user out or provide error message
except OAuthException as e:
# Handle other OAuth errors
print(f"OAuth error: {e.message}")
```

## Debugging APIs

Every method accepts a `debug` parameter that prints the equivalent cURL
Expand Down
69 changes: 69 additions & 0 deletions docs/LINTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Code Style and Linting

Linting and formatting are handled by [Black](https://github.com/psf/black),
[isort](https://github.com/pycqa/isort/),
[mdformat](https://github.com/executablebooks/mdformat),
[autoflake](https://github.com/PyCQA/autoflake) and a
[small script that adds a path comment](../lint/add_file_headers.py).

## Running the Linters

You can run all formatters using:

```bash
pdm run format
```

This will:

- Format Python code with Black
- Sort imports with isort
- Format Markdown files with mdformat
- Remove unused imports with autoflake
- Add file path headers to Python files

## Code Organization

Every Python file follows a precise organizational structure with three distinct
import sections:

1. **Standard library imports** - marked with `# Standard library imports`
2. **Third-party imports** - marked with `# Third party imports`
3. **Project-specific imports** - marked with `# Local imports`

Each section should be alphabetically ordered and separated by exactly one blank
line.

### Example

```python
"""Module docstring explaining the purpose of this file."""

# Standard library imports
from datetime import datetime
from inspect import currentframe
from json import JSONDecodeError
from json import dumps
from typing import Any
from typing import Dict

# Third party imports
from requests import Response
from requests_oauthlib import OAuth2Session

# Local imports
from fitbit_client.exceptions import FitbitAPIException
from fitbit_client.resources.base import BaseResource
```

## Documentation Requirements

The test suite verifies that all public methods have comprehensive docstrings
that follow the Google style format with specific sections:

- Args
- Returns
- Raises
- Note (if applicable)

Our linting tools ensure consistent style throughout the codebase.
87 changes: 84 additions & 3 deletions docs/NAMING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,96 @@ easier to find based on the official documentation.

## Inconsistencies in the API

The Fitbit API contains several inconsistencies, which our method names
necessarily reflect:
The Fitbit API contains several inconsistencies, which our method names and
implementation necessarily reflect. Understanding these inconsistencies can help
you navigate the API more effectively:

- `create_activity_goals` creates only one goal at a time
### Method Name vs. Functionality Inconsistencies

- `create_activity_goals` creates only one goal at a time, despite the plural
name
- `add_favorite_foods` adds one food at a time, while all other creation methods
use "create" prefix
- `get_sleep_goals` returns a single goal, not multiple goals
- Some pluralized methods return lists, while others return dictionaries
containing lists

### Parameter and Response Format Inconsistencies

```python
# Sleep endpoint uses a different API version than most other endpoints
client.sleep.get_sleep_log_by_date(date="2025-01-01") # Uses API v1.2
client.activity.get_daily_activity_summary(date="2025-01-01") # Uses API v1

# Sleep date parameters have inconsistent response behavior
# The request uses "2025-01-01" but response might contain data from "2025-01-02"
sleep_data = client.get_sleep_log_by_date(date="2025-01-01")
# A sleep log started on 2025-01-01 at 23:00 and ended on 2025-01-02 at 07:00
# will be included in the response, but with dateOfSleep="2025-01-02"

# Pagination parameters vary across endpoints
# Some endpoints require offset=0
food_log = client.get_food_log(date="2025-01-01", offset=0) # Valid
# Others support arbitrary offsets
badges = client.get_badges(offset=20) # Also valid

# Date range validations vary by endpoint
# Sleep endpoints allow up to 100 days
sleep_logs = client.get_sleep_log_by_date_range(
start_date="2025-01-01",
end_date="2025-04-10" # 100 days later, valid for sleep endpoint
)
# Activity endpoints allow only 31 days
activity_logs = client.get_activity_log_list(
before_date="2025-02-01",
after_date="2025-01-01" # 31 days earlier, valid for activity endpoint
)
```

### Response Structure Inconsistencies

The structure of API responses varies widely across endpoints:

```python
# Some endpoints return arrays directly
activities = client.get_frequent_activities()
# activities is a List[Dict[str, Any]] (array of activity objects)

# Others wrap arrays in a parent object with a named property
sleep_logs = client.get_sleep_log_by_date(date="2025-01-01")
# sleep_logs is a Dict[str, Any] with a "sleep" property containing the array

# Some endpoints use plural property names for lists
weight_logs = client.get_weight_logs(date="2025-01-01")
# weight_logs["weight"] is the list of weight logs

# Others use singular property names for lists
food_logs = client.get_food_log(date="2025-01-01")
# food_logs["foods"] is the list of food logs
```

### Error Response Inconsistencies

Error responses can also vary in structure:

```python
try:
# Some validation errors include field names
client.create_food_log(food_id="invalid", amount=100, meal_type_id=1)
except ValidationException as e:
print(e.field_name) # Might be "foodId"

try:
# Other validation errors omit field names
client.get_activity_tcx(log_id="invalid")
except InvalidRequestException as e:
print(e.field_name) # Might be None
```

Our library handles these inconsistencies internally to provide a unified
experience, but it's helpful to be aware of them when working with the raw API
responses.

## Method Aliases

For user convenience, some methods have aliases:
Expand Down
Loading