Skip to content

Commit ea582b4

Browse files
committed
Add pagination and rate limiting features
- Add PaginatedIterator class for navigating multi-page responses - Implement pagination in four endpoints: - `get_sleep_log_list()` - `get_activity_log_list()` - `get_ecg_log_list()` - `get_irn_alerts_list()` - Add automatic rate limit handling with proper backoff - Respect Fitbit's rate limit headers for retry timing - Support fallback to exponential backoff when headers not present - Add documentation for pagination and rate limiting
1 parent cebacf1 commit ea582b4

26 files changed

+2701
-186
lines changed

.gitignore

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,6 @@ htmlcov/
4646
.mypy_cache/
4747
*,cover
4848

49-
# Mac
50-
.DS_Store
51-
*,cover
52-
5349
# Mac
5450
.DS_Store
5551

README.md

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ OAuth2 PKCE authentication and resource-based API interactions.
1919

2020
## Installation
2121

22-
This package requires Python 3.13 or later.
22+
This package requires Python 3.13 (or later, when there is a later).
2323

2424
Once published, install like this:
2525

@@ -59,13 +59,13 @@ except Exception as e:
5959
```
6060

6161
The response will always be the body of the API response, and is almost always a
62-
`Dict`, `List` or `None`. `nutrition.get_activity_tcx` is the exception. It
63-
returns XML (as a `str`).
62+
`JSONDict`, `JSONList` or `None`. `nutrition.get_activity_tcx` is the exception.
63+
It returns XML (as a `str`).
6464

6565
## Method Aliases
6666

67-
All resource methods are available directly from the client instance. This means
68-
you can use:
67+
All API methods are available directly from the client instance. This means you
68+
can use:
6969

7070
```python
7171
# Short form with method aliases
@@ -87,7 +87,7 @@ Both approaches are equivalent, but aliases provide a more concise syntax.
8787

8888
## Authentication
8989

90-
Uses a local callback server to automatically handle the OAuth2 flow:
90+
Authentication wses a local callback server to handle the OAuth2 flow:
9191

9292
```python
9393
client = FitbitClient(
@@ -97,7 +97,7 @@ client = FitbitClient(
9797
token_cache_path="/tmp/fb_tokens.json"
9898
)
9999

100-
# Will open browser and handle callback automatically
100+
# This will open browser and handle callback automatically:
101101
client.authenticate()
102102
```
103103

@@ -129,7 +129,10 @@ Where secrets.json contains:
129129
}
130130
```
131131

132-
You can also include the optional token_cache_path:
132+
Using this strategy, you can initialize the client with several additional
133+
parameter arguments (such as [Rate Limiting](#rate-limiting) and language/locale
134+
options; see the [FitbitClient](fitbit_client/client.py) initializer). Perhaps
135+
the most useful of these is the `token_cache_path`:
133136

134137
```json
135138
{
@@ -144,18 +147,65 @@ The `token_cache_path` parameter allows you to persist authentication tokens
144147
between sessions. If provided, the client will:
145148

146149
1. Load existing tokens from this file if available (avoiding re-authentication)
147-
148150
2. Save new or refreshed tokens to this file automatically
149-
150151
3. Handle token refresh when expired tokens are detected
151152

152153
## Setting Up Your Fitbit App
153154

154155
1. Go to dev.fitbit.com and create a new application
155-
2. Set OAuth 2.0 Application Type to "Personal"
156+
2. Set OAuth 2.0 Application Type to "Personal" (or other types, if you know
157+
what you're doing)
156158
3. Set Callback URL to "https://localhost:8080" (or your preferred local URL)
157159
4. Copy your Client ID and Client Secret
158160

161+
## Pagination
162+
163+
Some Fitbit API endpoints support pagination for large result sets. With this
164+
client, you can work with paginated endpoints in two ways:
165+
166+
```python
167+
# Standard way - get a single page of results
168+
sleep_logs = client.get_sleep_log_list(before_date="2025-01-01")
169+
170+
# Iterator way - get an iterator that fetches all pages automatically
171+
for page in client.get_sleep_log_list(before_date="2025-01-01", as_iterator=True):
172+
for sleep_entry in page["sleep"]:
173+
print(sleep_entry["logId"])
174+
```
175+
176+
Endpoints that support pagination:
177+
178+
- `get_sleep_log_list()`
179+
- `get_activity_log_list()`
180+
- `get_ecg_log_list()`
181+
- `get_irn_alerts_list()`
182+
183+
For more details, see [PAGINATION.md](docs/PAGINATION.md).
184+
185+
## Rate Limiting
186+
187+
The client includes automatic retry handling for rate-limited requests. When a
188+
rate limit is encountered, the client will:
189+
190+
1. Log the rate limit event
191+
2. Wait using an exponential backoff strategy
192+
3. Automatically retry the request
193+
194+
You can configure rate limiting behavior:
195+
196+
```python
197+
client = FitbitClient(
198+
client_id="YOUR_CLIENT_ID",
199+
client_secret="YOUR_CLIENT_SECRET",
200+
redirect_uri="https://localhost:8080",
201+
max_retries=5, # Maximum number of retry attempts (default: 3)
202+
retry_after_seconds=30, # Base wait time in seconds (default: 60)
203+
retry_backoff_factor=2.0 # Multiplier for successive waits (default: 1.5)
204+
)
205+
```
206+
207+
For more details, see [RATE_LIMITING.md](docs/RATE_LIMITING.md).
208+
159209
## Additional Documentation
160210

161211
### For API Library Users
@@ -165,6 +215,9 @@ between sessions. If provided, the client will:
165215
- [NAMING.md](docs/NAMING.md): API method naming conventions
166216
- [VALIDATIONS.md](docs/VALIDATIONS.md): Input parameter validation
167217
- [ERROR_HANDLING.md](docs/ERROR_HANDLING.md): Exception hierarchy and handling
218+
- [PAGINATION.md](docs/PAGINATION.md): Working with paginated endpoints
219+
- [RATE_LIMITING.md](docs/RATE_LIMITING.md): Rate limit handling and
220+
configuration
168221

169222
It's also worth reviewing
170223
[Fitbit's Best Practices](https://dev.fitbit.com/build/reference/web-api/developer-guide/best-practices/)

docs/DEVELOPMENT.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,33 @@ The OAuth callback mechanism is implemented using two main classes:
307307

308308
This section will be documented as we near our first release.
309309

310+
## Pagination Implementation
311+
312+
The pagination implementation uses the following approach:
313+
314+
### Pagination Iterator
315+
316+
- Uses the `PaginatedIterator` class that implements the Python `Iterator`
317+
protocol
318+
- Automatically handles fetching the next page when needed using the `next` URL
319+
from pagination metadata
320+
- Properly handles edge cases like invalid responses, missing pagination data,
321+
and API errors
322+
323+
### Type Safety
324+
325+
- Uses `TYPE_CHECKING` from the typing module to avoid circular imports at
326+
runtime
327+
- Maintains complete type safety and mypy compatibility
328+
- All pagination-related code has 100% test coverage
329+
330+
### Resource Integration
331+
332+
Each endpoint that supports pagination has an `as_iterator` parameter that, when
333+
set to `True`, returns a `PaginatedIterator` instead of the raw API response.
334+
This makes it easy to iterate through all pages of results without manually
335+
handling pagination.
336+
310337
## Intraday Data Support
311338

312339
This client implements intraday data endpoints (detailed heart rate, steps, etc)

docs/NAMING.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
## Naming Principles
44

5-
The method names in this library are designed to align with the official Fitbit
6-
Web API Documentation. When there are inconsistencies in the official
7-
documentation, we prioritize the URL slug. For example, if the documentation
8-
page title says "Get **Time Series** by Date" but the URL is
5+
The API method names are designed to align with the official Fitbit Web API
6+
Documentation. When there are inconsistencies in the official documentation, we
7+
prefer the URL slug for the page. For example, if the documentation page title
8+
says "Get **Time Series** by Date" but the URL is
99
".../get-azm-timeseries-by-date/", our method will be named
1010
`get_azm_timeseries_by_date()`. (not `get_azm_time_series_by_date()`).
1111

@@ -20,13 +20,16 @@ you navigate the API more effectively:
2020

2121
### Method Name vs. Functionality Inconsistencies
2222

23+
Examples:
24+
2325
- `create_activity_goals` creates only one goal at a time, despite the plural
2426
name
25-
- `add_favorite_foods` adds one food at a time, while all other creation methods
26-
start with "create".
27+
- `add_favorite_foods` adds one food at a time; also, all other creation/POST
28+
methods start with "`create_`".
2729
- `get_sleep_goals` returns a single goal, not multiple goals
2830
- Additionally, some pluralized methods return lists, while others return
29-
dictionaries containing lists
31+
dictionaries containing lists (see
32+
[Response Structure Inconsistencies](#response-structure-inconsistencies))
3033

3134
For user convenience, these inconsistencies have aliases:
3235

docs/PAGINATION.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Pagination
2+
3+
Some API endpoints return potentially large result sets and support pagination.
4+
We provide an easy and pythonic way to work with these paginated endpoints as
5+
iterators.
6+
7+
## Supported Endpoints
8+
9+
The following endpoints support pagination:
10+
11+
- `client.get_sleep_log_list()`
12+
- `client.get_activity_log_list()`
13+
- `client.get_ecg_log_list()`
14+
- `client.get_irn_alerts_list()`
15+
16+
## Usage
17+
18+
### Standard Mode
19+
20+
By default, all endpoints return a single page of results with pagination
21+
metadata:
22+
23+
```python
24+
# Get a single (the first) page of sleep logs
25+
sleep_data = client.get_sleep_log_list(
26+
before_date="2025-01-01",
27+
sort=SortDirection.DESCENDING,
28+
limit=10
29+
)
30+
```
31+
32+
### Iterator Mode
33+
34+
When you need to process multiple pages of data, use iterator mode:
35+
36+
```python
37+
iterator = client.get_sleep_log_list(
38+
before_date="2025-01-01",
39+
sort=SortDirection.DESCENDING,
40+
limit=10,
41+
as_iterator=True # Creates an iterator for all pages
42+
)
43+
44+
# Process all pages - the iterator fetches new pages as needed
45+
for page in iterator:
46+
# Each page has the same structure as the standard response
47+
for sleep_entry in page["sleep"]:
48+
print(f"Sleep log ID: {sleep_entry['logId']}")
49+
```
50+
51+
## Pagination Parameters
52+
53+
Different endpoints support different pagination parameters, but they generally
54+
follow these patterns:
55+
56+
| Parameter | Description | Constraints |
57+
| ------------- | ------------------------------- | ----------------------------------------------------------- |
58+
| `before_date` | Return entries before this date | Must use with `sort=SortDirection.DESCENDING` |
59+
| `after_date` | Return entries after this date | Must use with `sort=SortDirection.ASCENDING` |
60+
| `limit` | Maximum items per page | Varies by endpoint (10-100) |
61+
| `offset` | Starting position | Usually only `0` is supported |
62+
| `sort` | Sort direction | Use `SortDirection.ASCENDING` or `SortDirection.DESCENDING` |
63+
64+
## Endpoint-Specific Notes
65+
66+
Each paginated endpoint has specific constraints:
67+
68+
### `get_sleep_log_list`
69+
70+
- Max limit: 100 entries per page
71+
- Date filtering: `before_date` or `after_date` (must specify one but not both)
72+
73+
### `get_activity_log_list`
74+
75+
- Max limit: 100 entries per page
76+
- Date filtering: `before_date` or `after_date` (must specify one but not both)
77+
78+
### `get_ecg_log_list` and `get_irn_alerts_list`
79+
80+
- Max limit: 10 entries per page

0 commit comments

Comments
 (0)