Skip to content

Commit 1b92986

Browse files
committed
continuing on with documentation review
1 parent 6efc808 commit 1b92986

File tree

6 files changed

+245
-258
lines changed

6 files changed

+245
-258
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ for API usage.
173173
### Project Best Practices
174174

175175
- [DEVELOPMENT.md](docs/DEVELOPMENT.md): Development environment and guidelines
176-
- [STYLE.md](docs/STYLE.md): Code style and formatting standards
176+
- [LINTING.md](docs/LINTING.md): Code style and linting configuration
177177

178178
## Important Note - Subscription Support
179179

docs/DEVELOPMENT.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
- [Response Mocking](#response-mocking)
2727
- [OAuth Callback Implementation](#oauth-callback-implementation)
2828
- [Implementation Flow](#implementation-flow)
29-
- [Security Notes](#security-notes)
3029
- [Git Workflow](#git-workflow)
3130
- [Release Process](#release-process)
3231
- [Getting Help](#getting-help)
@@ -45,8 +44,8 @@
4544
1. Clone the repository:
4645

4746
```bash
48-
git clone https://github.com/yourusername/fitbit-client.git
49-
cd fitbit-client
47+
git clone https://github.com/jpstroop/fitbit-client-python.git
48+
cd fitbit-client-python
5049
```
5150

5251
2. Install asdf plugins and required versions:

docs/ERROR_HANDLING.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,95 @@ except FitbitAPIException as e:
163163
print(f"API error: {e.message}") # Catches all other API errors
164164
```
165165

166+
### Handling OAuth-Specific Exceptions
167+
168+
OAuth errors require special handling. Here's how to handle different OAuth
169+
exception subtypes:
170+
171+
```python
172+
from fitbit_client.exceptions import ExpiredTokenException, InvalidTokenException
173+
from fitbit_client.exceptions import InvalidClientException, InvalidGrantException
174+
175+
try:
176+
# Make an API call that requires authentication
177+
client.get_profile()
178+
except ExpiredTokenException as e:
179+
# Token has expired but couldn't be auto-refreshed
180+
print(f"Token expired: {e.message}")
181+
# Attempt to re-authenticate
182+
client.authenticate()
183+
except InvalidTokenException as e:
184+
# Token is invalid (e.g., revoked by user)
185+
print(f"Token invalid: {e.message}")
186+
# Re-authenticate from scratch
187+
client.authenticate()
188+
except InvalidClientException as e:
189+
# Client credentials are incorrect
190+
print(f"Client credentials error: {e.message}")
191+
# Check client_id and client_secret
192+
except InvalidGrantException as e:
193+
# Refresh token is invalid
194+
print(f"Invalid refresh token: {e.message}")
195+
# Re-authenticate to get a new refresh token
196+
client.authenticate()
197+
```
198+
199+
## Token Refresh Strategies
200+
201+
The client automatically handles token refresh when tokens expire. However, you
202+
may want to implement custom token refresh strategies for your application.
203+
204+
### Automatic Token Refresh
205+
206+
By default, the client refreshes tokens automatically:
207+
208+
1. When initializing the client with cached tokens
209+
2. During API calls when a token expires
210+
3. When explicitly calling a method that requires authentication
211+
212+
```python
213+
# Tokens are automatically refreshed
214+
client = FitbitClient(
215+
client_id="YOUR_CLIENT_ID",
216+
client_secret="YOUR_CLIENT_SECRET",
217+
redirect_uri="https://localhost:8080",
218+
token_cache_path="/path/to/tokens.json" # Enable persistent token caching
219+
)
220+
221+
# If cached tokens exist and are valid or can be refreshed, no browser prompt occurs
222+
client.authenticate()
223+
224+
# If the token expires during this call, it will be refreshed automatically
225+
client.get_profile()
226+
```
227+
228+
### Handling Failed Token Refresh
229+
230+
When automatic token refresh fails, the client raises an appropriate OAuth
231+
exception. Here's a complete error handling pattern:
232+
233+
```python
234+
from fitbit_client.exceptions import OAuthException, ExpiredTokenException
235+
236+
try:
237+
result = client.get_profile()
238+
# Process result normally
239+
except ExpiredTokenException:
240+
# Token expired and auto-refresh failed
241+
try:
242+
# Try to authenticate again
243+
client.authenticate()
244+
# Retry the original request
245+
result = client.get_profile()
246+
except OAuthException as oauth_error:
247+
# Handle authentication failure (e.g., user closed browser window)
248+
print(f"Authentication failed: {oauth_error.message}")
249+
# Log the user out or provide error message
250+
except OAuthException as e:
251+
# Handle other OAuth errors
252+
print(f"OAuth error: {e.message}")
253+
```
254+
166255
## Debugging APIs
167256

168257
Every method accepts a `debug` parameter that prints the equivalent cURL

docs/LINTING.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Code Style and Linting
2+
3+
Linting and formatting are handled by [Black](https://github.com/psf/black),
4+
[isort](https://github.com/pycqa/isort/),
5+
[mdformat](https://github.com/executablebooks/mdformat),
6+
[autoflake](https://github.com/PyCQA/autoflake) and a
7+
[small script that adds a path comment](../lint/add_file_headers.py).
8+
9+
## Running the Linters
10+
11+
You can run all formatters using:
12+
13+
```bash
14+
pdm run format
15+
```
16+
17+
This will:
18+
19+
- Format Python code with Black
20+
- Sort imports with isort
21+
- Format Markdown files with mdformat
22+
- Remove unused imports with autoflake
23+
- Add file path headers to Python files
24+
25+
## Code Organization
26+
27+
Every Python file follows a precise organizational structure with three distinct
28+
import sections:
29+
30+
1. **Standard library imports** - marked with `# Standard library imports`
31+
2. **Third-party imports** - marked with `# Third party imports`
32+
3. **Project-specific imports** - marked with `# Local imports`
33+
34+
Each section should be alphabetically ordered and separated by exactly one blank
35+
line.
36+
37+
### Example
38+
39+
```python
40+
"""Module docstring explaining the purpose of this file."""
41+
42+
# Standard library imports
43+
from datetime import datetime
44+
from inspect import currentframe
45+
from json import JSONDecodeError
46+
from json import dumps
47+
from typing import Any
48+
from typing import Dict
49+
50+
# Third party imports
51+
from requests import Response
52+
from requests_oauthlib import OAuth2Session
53+
54+
# Local imports
55+
from fitbit_client.exceptions import FitbitAPIException
56+
from fitbit_client.resources.base import BaseResource
57+
```
58+
59+
## Documentation Requirements
60+
61+
The test suite verifies that all public methods have comprehensive docstrings
62+
that follow the Google style format with specific sections:
63+
64+
- Args
65+
- Returns
66+
- Raises
67+
- Note (if applicable)
68+
69+
Our linting tools ensure consistent style throughout the codebase.

docs/NAMING.md

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,96 @@ easier to find based on the official documentation.
2828

2929
## Inconsistencies in the API
3030

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

34-
- `create_activity_goals` creates only one goal at a time
35+
### Method Name vs. Functionality Inconsistencies
36+
37+
- `create_activity_goals` creates only one goal at a time, despite the plural
38+
name
3539
- `add_favorite_foods` adds one food at a time, while all other creation methods
3640
use "create" prefix
41+
- `get_sleep_goals` returns a single goal, not multiple goals
3742
- Some pluralized methods return lists, while others return dictionaries
3843
containing lists
3944

45+
### Parameter and Response Format Inconsistencies
46+
47+
```python
48+
# Sleep endpoint uses a different API version than most other endpoints
49+
client.sleep.get_sleep_log_by_date(date="2025-01-01") # Uses API v1.2
50+
client.activity.get_daily_activity_summary(date="2025-01-01") # Uses API v1
51+
52+
# Sleep date parameters have inconsistent response behavior
53+
# The request uses "2025-01-01" but response might contain data from "2025-01-02"
54+
sleep_data = client.get_sleep_log_by_date(date="2025-01-01")
55+
# A sleep log started on 2025-01-01 at 23:00 and ended on 2025-01-02 at 07:00
56+
# will be included in the response, but with dateOfSleep="2025-01-02"
57+
58+
# Pagination parameters vary across endpoints
59+
# Some endpoints require offset=0
60+
food_log = client.get_food_log(date="2025-01-01", offset=0) # Valid
61+
# Others support arbitrary offsets
62+
badges = client.get_badges(offset=20) # Also valid
63+
64+
# Date range validations vary by endpoint
65+
# Sleep endpoints allow up to 100 days
66+
sleep_logs = client.get_sleep_log_by_date_range(
67+
start_date="2025-01-01",
68+
end_date="2025-04-10" # 100 days later, valid for sleep endpoint
69+
)
70+
# Activity endpoints allow only 31 days
71+
activity_logs = client.get_activity_log_list(
72+
before_date="2025-02-01",
73+
after_date="2025-01-01" # 31 days earlier, valid for activity endpoint
74+
)
75+
```
76+
77+
### Response Structure Inconsistencies
78+
79+
The structure of API responses varies widely across endpoints:
80+
81+
```python
82+
# Some endpoints return arrays directly
83+
activities = client.get_frequent_activities()
84+
# activities is a List[Dict[str, Any]] (array of activity objects)
85+
86+
# Others wrap arrays in a parent object with a named property
87+
sleep_logs = client.get_sleep_log_by_date(date="2025-01-01")
88+
# sleep_logs is a Dict[str, Any] with a "sleep" property containing the array
89+
90+
# Some endpoints use plural property names for lists
91+
weight_logs = client.get_weight_logs(date="2025-01-01")
92+
# weight_logs["weight"] is the list of weight logs
93+
94+
# Others use singular property names for lists
95+
food_logs = client.get_food_log(date="2025-01-01")
96+
# food_logs["foods"] is the list of food logs
97+
```
98+
99+
### Error Response Inconsistencies
100+
101+
Error responses can also vary in structure:
102+
103+
```python
104+
try:
105+
# Some validation errors include field names
106+
client.create_food_log(food_id="invalid", amount=100, meal_type_id=1)
107+
except ValidationException as e:
108+
print(e.field_name) # Might be "foodId"
109+
110+
try:
111+
# Other validation errors omit field names
112+
client.get_activity_tcx(log_id="invalid")
113+
except InvalidRequestException as e:
114+
print(e.field_name) # Might be None
115+
```
116+
117+
Our library handles these inconsistencies internally to provide a unified
118+
experience, but it's helpful to be aware of them when working with the raw API
119+
responses.
120+
40121
## Method Aliases
41122

42123
For user convenience, some methods have aliases:

0 commit comments

Comments
 (0)