Skip to content

Commit f6b183e

Browse files
authored
Merge pull request #23 from jpstroop/docs
Docs
2 parents 03bed90 + 8e5ac97 commit f6b183e

27 files changed

+74
-206
lines changed

docs/DEVELOPMENT.md

Lines changed: 35 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Python 3.13+
88
- PDM
99
- Git
10+
- ASDF (recommended)
1011

1112
### Download and Install the Source Code
1213

@@ -49,8 +50,9 @@ fitbit-client/
4950
│ ├── resources/
5051
│ │ ├── __init__.py
5152
│ │ ├── [resource modules]
52-
│ │ ├── base.py
53-
│ │ └── constants.py
53+
│ │ ├── _base.py
54+
│ │ ├── _pagination.py
55+
│ │ └── _constants.py
5456
│ ├── utils/
5557
│ │ ├── __init__.py
5658
│ │ ├── curl_debug_mixin.py
@@ -59,26 +61,22 @@ fitbit-client/
5961
│ │ ├── pagination_validation.py
6062
│ │ └── types.py
6163
│ └── exceptions.py
62-
├── tests/
63-
│ ├── auth/
64-
│ ├── resources/
65-
│ └── utils/
66-
└── [project files]
64+
└── tests/
65+
│ ├── fitbit_client/
66+
│ ├── auth/
67+
│ ├── resources/
68+
│ └── utils/
69+
└── [project files]
6770
```
6871

69-
## Goals, Notes, and TODOs
70-
71-
For now these are just in [TODO.md](TODO.md); bigger work will eventually move
72-
to Github tickets.
73-
7472
## Development Tools and Standards
7573

7674
### Code Formatting and Style
7775

7876
- Black for code formatting (100 character line length)
7977
- isort for import sorting
80-
- Type hints required for all code
81-
- Docstrings required for all public methods
78+
- Type hints required for all code (enforced by `mypy`)
79+
- Docstrings required for all public methods (enforced by `test_docscrings.py`)
8280

8381
### Import Style
8482

@@ -102,7 +100,7 @@ import typing
102100
import datetime
103101
```
104102

105-
The one exception to this rule is when an entire module needs to be `mock`ed for
103+
The one exception to this rule is when an entire module needs to be mocked for
106104
testing, in which case, at least for the `json` package from the standard
107105
library, the entire package has to be imported. So `import json` is ok when that
108106
circumstance arises.
@@ -120,46 +118,29 @@ Follow your nose from `client.py` and the structure should be very clear.
120118
#### Method Structure
121119

122120
- Include comprehensive docstrings with Args sections
123-
- Keep parameter naming consistent across methods
124-
- Use "-" as default for user_id parameters
125-
- Return Dict[str, Any] for most methods that return data
126-
- Return None for delete operations
121+
- Keep parameter naming consistent across methods (see [Naming](docs/NAMING.md))
122+
- Return `JSONDict` for `JSONList` for most methods (`get_activity_tcx` returns
123+
XML as a string)
124+
- Return `None` for delete operations
127125

128126
### Error Handling
129127

130-
The codebase implements a comprehensive error handling system through
131-
[`exceptions.py`](fitbit_client/exceptions.py):
132-
133-
1. A base FitbitAPIException that captures:
134-
135-
- HTTP status code
136-
- Error type
137-
- Error message
138-
- Field name (when applicable)
139-
140-
2. Specialized exceptions for different error scenarios:
141-
142-
- InvalidRequestException for malformed requests
143-
- ValidationException for parameter validation failures
144-
- AuthorizationException for authentication issues
145-
- RateLimitExceededException for API throttling
146-
- SystemException for server-side errors
147-
148-
3. Mapping from HTTP status codes and API error types to appropriate exception
149-
classes
128+
The codebase implements a comprehensive error handling system. See
129+
[ERROR_HANDLING](docs/ERROR_HANDLING.md) and
130+
[`exceptions.py`](fitbit_client/exceptions.py).
150131

151132
### Enum Usage
152133

153134
- Only use enums for validating request parameters, not responses
154-
- Place all enums in constants.py
135+
- Place all enums in [`constants.py`](fitbit_client/resources/_constants.py)
155136
- Only import enums that are actively used in the class
156137

157138
## Logging System
158139

159140
The project implements two different logs in through the
160-
[`BaseResource`](fitbit_client/resources/base.py) class: application logging for
161-
API interactions and data logging for tracking important response fields. See
162-
[LOGGING](docs/LOGGING.md) for details.
141+
[`BaseResource`](fitbit_client/resources/_base.py) class: application logging
142+
for API interactions and data logging for tracking important response fields.
143+
See [LOGGING](docs/LOGGING.md) for details.
163144

164145
## API Design
165146

@@ -190,77 +171,27 @@ client.get_profile()
190171
client.get_daily_activity_summary(date="2025-03-06")
191172
```
192173

193-
Method aliases were implemented for several important reasons:
194-
195-
1. **Reduced Verbosity**: Typing `client.resource_name.method_name(...)` with
196-
many parameters can be tedious, especially when used frequently.
197-
198-
2. **Flatter API Surface**: Many modern APIs prefer a flatter design that avoids
199-
deep nesting, making the API more straightforward to use.
200-
201-
3. **Method Name Uniqueness**: All resource methods in the Fitbit API have
202-
unique names (e.g., there's only one `get_profile()` method), making it safe
203-
to expose these methods directly on the client.
204-
205-
4. **Preserve Both Options**: By maintaining both the resource-based access and
206-
direct aliases, developers can choose the approach that best fits their needs
207-
\- organization or conciseness.
208-
209-
All method aliases are set up in the `_set_up_method_aliases()` method in the
174+
Method aliases were implemented because yyping
175+
`client.resource_name.method_name(...)` with many parameters can be tedious,
176+
especially when used frequently. All method aliases are set up in the
177+
`_set_up_method_aliases()` method in the
210178
[`FitbitClient`](fitbit_client/client.py) class, which is called during
211179
initialization. Each alias is a direct reference to the corresponding resource
212180
method, ensuring consistent behavior regardless of how the method is accessed.
213181

214182
## Testing
215183

216184
The project uses pytest for testing and follows a consistent testing approach
217-
across all components.
185+
across all components. 100% coverage is expected.
218186

219187
### Test Organization
220188

221-
The test directory mirrors the main package structure (except that the root is
222-
named "test" rather than "fitbit_client"), with corresponding test modules for
223-
each component:
224-
225-
- auth/: Tests for authentication and OAuth functionality
226-
- client/: Tests for the main client implementation
227-
- resources/: Tests for individual API resource implementations
228-
229-
### Standard Test Fixtures
230-
231-
The test suite provides several standard fixtures for use across test modules:
189+
The test directory mirrors the main package structure within the `test`
190+
directory. For the most part, the naming is 1:1 (`test_blah.py`) or otherwise
191+
obvious--many tests modules were getting quite long and broken out either into
192+
directories or with names that make it obvious as to hwat they are testing.
232193

233-
```python
234-
@fixture
235-
def mock_oauth_session():
236-
"""Provides a mock OAuth session for testing resources"""
237-
return Mock()
238-
239-
@fixture
240-
def mock_logger():
241-
"""Provides a mock logger for testing logging behavior"""
242-
return Mock()
243-
244-
@fixture
245-
def base_resource(mock_oauth_session, mock_logger):
246-
"""Creates a resource instance with mocked dependencies"""
247-
with patch("fitbit_client.resources._base.getLogger", return_value=mock_logger):
248-
return BaseResource(mock_oauth_session, "en_US", "en_US")
249-
```
250-
251-
### Error Handling Tests
252-
253-
Tests verify proper error handling across the codebase. Common patterns include:
254-
255-
```python
256-
def test_http_error_handling(resource):
257-
"""Tests that HTTP errors are properly converted to exceptions"""
258-
with raises(InvalidRequestException) as exc_info:
259-
# Test code that should raise the exception
260-
pass
261-
assert exc_info.value.status_code == 400
262-
assert exc_info.value.error_type == "validation"
263-
```
194+
All resource mocks are in the root [conftest.py](tests/conftest.py).
264195

265196
### Response Mocking
266197

@@ -332,39 +263,4 @@ git commit --no-verify -m "Your commit message"
332263

333264
## Release Process
334265

335-
This section will be documented as we near our first release.
336-
337-
## Pagination Implementation
338-
339-
The pagination implementation uses the following approach:
340-
341-
### Pagination Iterator
342-
343-
- Uses the `PaginatedIterator` class that implements the Python `Iterator`
344-
protocol
345-
- Automatically handles fetching the next page when needed using the `next` URL
346-
from pagination metadata
347-
- Properly handles edge cases like invalid responses, missing pagination data,
348-
and API errors
349-
350-
### Type Safety
351-
352-
- Uses `TYPE_CHECKING` from the typing module to avoid circular imports at
353-
runtime
354-
- Maintains complete type safety and mypy compatibility
355-
- All pagination-related code has 100% test coverage
356-
357-
### Resource Integration
358-
359-
Each endpoint that supports pagination has an `as_iterator` parameter that, when
360-
set to `True`, returns a `PaginatedIterator` instead of the raw API response.
361-
This makes it easy to iterate through all pages of results without manually
362-
handling pagination.
363-
364-
## Intraday Data Support
365-
366-
This client implements intraday data endpoints (detailed heart rate, steps, etc)
367-
through the `IntradayResource` class. These endpoints have some special
368-
requirements if you're using them for anyone other that yourself. See the
369-
[Intraday API documentation](https://dev.fitbit.com/build/reference/web-api/intraday/)
370-
for more details.
266+
_This section will be documented as we near our first release._

docs/RATE_LIMITING.md

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ client = FitbitClient(
4949

5050
# Rate limiting options (all optional)
5151
max_retries=5, # Maximum retry attempts (default: 3)
52-
retry_after_seconds=30, # Base wait time if headers missing (default: 60)
53-
retry_backoff_factor=2.0 # Multiplier for successive waits (default: 1.5)
52+
retry_after_seconds=60, # Base wait time if headers missing (default: 60)
53+
retry_backoff_factor=1.5 # Multiplier for successive waits (default: 1.5)
5454
)
5555
```
5656

@@ -68,11 +68,13 @@ The client uses the following strategy for retries:
6868
retry_time = retry_after_seconds * (retry_backoff_factor ^ retry_count)
6969
```
7070

71-
With the default settings and no headers:
71+
With example settings of 5 retries and no headers:
7272

73-
- First retry: Wait 60 seconds
74-
- Second retry: Wait 90 seconds (60 * 1.5)
73+
- First retry: Wait 60 seconds (base time)
74+
- Second retry: Wait 90 seconds (60 * 1.5¹)
7575
- Third retry: Wait 135 seconds (60 * 1.5²)
76+
- Fourth retry: Wait 202.5 seconds (60 * 1.5³)
77+
- Fifth retry: Wait 303.75 seconds (60 * 1.5⁴)
7678

7779
## Logging
7880

@@ -92,7 +94,7 @@ client = FitbitClient(...)
9294
You'll see log messages like:
9395

9496
```
95-
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)
97+
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)
9698
```
9799

98100
## Handling Unrecoverable Rate Limits
@@ -132,3 +134,27 @@ except RateLimitExceededException as e:
132134

133135
These can be used to implement more sophisticated retry or backoff strategies in
134136
your application.
137+
138+
## Advanced Usage
139+
140+
You can implement custom strategies by combining rate limit information with
141+
your own timing logic:
142+
143+
```python
144+
from datetime import datetime, timedelta
145+
from time import sleep
146+
147+
try:
148+
client.get_daily_activity_summary(date="today")
149+
except RateLimitExceededException as e:
150+
# Calculate next reset time (typically the top of the next hour)
151+
reset_time = datetime.now() + timedelta(seconds=e.rate_limit_reset)
152+
print(f"Rate limit reached. Pausing until {reset_time.strftime('%H:%M:%S')}")
153+
154+
# Wait until reset time plus a small buffer
155+
wait_seconds = e.rate_limit_reset + 5
156+
sleep(wait_seconds)
157+
158+
# Try again after waiting
159+
client.get_daily_activity_summary(date="today")
160+
```

docs/TYPES.md

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

33
## Overview
44

5-
Strong typing JSON is complicated. The primary goal for typing in this library
6-
is to help you at least understand at least the outermost data structure of the
7-
API responses. All resource methods (API endpoints) return one of three types:
5+
Strong typing JSON is complicated to do in any meaningful way. In our case, the
6+
primary goal for typing is to help you at least understand at least the
7+
outermost data structure of the API responses. All resource methods (API
8+
endpoints) return one of three types:
89

910
- `JSONDict`: A dictionary containing JSON data
1011
- `JSONList`: A list containing JSON data

fitbit_client/auth/callback_handler.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,9 @@
55
from http.server import HTTPServer
66
from logging import Logger
77
from logging import getLogger
8-
from socket import socket
9-
from typing import Any # Used only for type declarations, not in runtime code
10-
from typing import Callable
8+
from typing import Any
119
from typing import Dict
1210
from typing import List
13-
from typing import Tuple
14-
from typing import Type
1511
from typing import TypeVar
1612
from typing import Union
1713
from urllib.parse import parse_qs
@@ -20,7 +16,6 @@
2016
# Local imports
2117
from fitbit_client.exceptions import InvalidGrantException
2218
from fitbit_client.exceptions import InvalidRequestException
23-
from fitbit_client.utils.types import JSONDict
2419

2520
# Type variable for server
2621
T = TypeVar("T", bound=HTTPServer)

fitbit_client/auth/oauth.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,8 @@
2121

2222
# Local imports
2323
from fitbit_client.auth.callback_server import CallbackServer
24-
from fitbit_client.exceptions import ExpiredTokenException
25-
from fitbit_client.exceptions import InvalidClientException
2624
from fitbit_client.exceptions import InvalidGrantException
2725
from fitbit_client.exceptions import InvalidRequestException
28-
from fitbit_client.exceptions import InvalidTokenException
2926
from fitbit_client.utils.types import TokenDict
3027

3128

fitbit_client/client.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,6 @@
88
# isort: off
99
# Auth imports
1010
from fitbit_client.auth.oauth import FitbitOAuth2
11-
from fitbit_client.exceptions import ExpiredTokenException
12-
from fitbit_client.exceptions import InvalidClientException
13-
from fitbit_client.exceptions import InvalidGrantException
14-
from fitbit_client.exceptions import InvalidRequestException
15-
from fitbit_client.exceptions import InvalidTokenException
1611
from fitbit_client.exceptions import OAuthException
1712
from fitbit_client.exceptions import SystemException
1813

fitbit_client/exceptions.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
# fitbit_client/exceptions.py
22

33
# Standard library imports
4-
from typing import Any
5-
from typing import Dict
64
from typing import List
75
from typing import Optional
86
from typing import TYPE_CHECKING

0 commit comments

Comments
 (0)