Skip to content

Commit 34e8f50

Browse files
authored
Big exception refactor and simplification (#11)
* Big exception refactor and simplification * fix test coverage
1 parent 3460f4b commit 34e8f50

26 files changed

+592
-223
lines changed

TODO.md

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,10 @@ if not food_id and not (food_name and calories):
4545

4646
see: test_create_food_calories_from_fat_must_be_integer(nutrition_resource)
4747

48-
- exceptions.py
49-
50-
- Should ClientValidationException really subclass FitbitAPIException? IT
51-
SHOULD SUBCLASS ValueError doesn't need the API lookup mapping
52-
(`exception_type`) or a `status_code`, so we may just be able to simplify
53-
it. The most important thing is that the user understands that the message
54-
came from the client prior to the API call.
55-
56-
- Make sure we aren't using
57-
58-
- Make sure that `ClientValidationException` is getting used for arbitrary
59-
validations like
48+
- exceptions.py Consider:
49+
- Add automatic token refresh for ExpiredTokenException
50+
- Implement backoff and retry for RateLimitExceededException
51+
- Add retry with exponential backoff for transient errors (5xx)
6052

6153
## Longer term TODOs
6254

docs/VALIDATIONS_AND_EXCEPTIONS.md

Lines changed: 97 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
# Input Validation and Error Handling
22

3-
Many method parameter arguments are validated before making API requests. The
4-
aim is to encapulate the HTTP API as much as possible and raise more helpfule
5-
exceptions before a bad request is executed. Understanding these validations and
6-
the exceptions that are raised by them (and elsewhere) will help you use this
7-
library correctly.
3+
Many method parameter arguments are validated **before making any API
4+
requests**. The aim is to encapsulate the HTTP API as much as possible and raise
5+
more helpful exceptions before a bad request is executed. This approach:
6+
7+
- Preserves your API rate limits by catching errors locally
8+
- Provides more specific and helpful error messages
9+
- Simplifies debugging by clearly separating client-side validation issues from
10+
API response issues
11+
12+
Understanding these validations and the exceptions that are raised by them (and
13+
elsewhere) will help you use this library correctly and efficiently.
814

915
## Input Validation
1016

@@ -167,9 +173,52 @@ except ValidationException as e:
167173

168174
## Exception Handling
169175

170-
There are many custom exceptions, When validation fails or other errors occur,
176+
There are many custom exceptions. When validation fails or other errors occur,
171177
the library raises specific exceptions that help identify the problem.
172178

179+
### Using Custom Validation Exceptions
180+
181+
Client validation exceptions (`ClientValidationException` and its subclasses)
182+
are raised *before* any API call is made. This means:
183+
184+
1. They reflect problems with your input parameters that can be detected locally
185+
2. No network requests have been initiated when these exceptions occur
186+
3. They help you fix issues before consuming API rate limits
187+
188+
This is in contrast to API exceptions (`FitbitAPIException` and its subclasses),
189+
which are raised in response to errors returned by the Fitbit API after a
190+
network request has been made.
191+
192+
When using this library, you'll want to catch the specific exception types for
193+
proper error handling:
194+
195+
```python
196+
from fitbit_client.exceptions import ParameterValidationException, MissingParameterException
197+
198+
try:
199+
# When parameters might be missing
200+
client.nutrition.create_food_goal(calories=None, intensity=None)
201+
except MissingParameterException as e:
202+
print(f"Missing parameter: {e.message}")
203+
204+
try:
205+
# When parameters might be invalid
206+
client.sleep.create_sleep_goals(min_duration=-10)
207+
except ParameterValidationException as e:
208+
print(f"Invalid parameter value for {e.field_name}: {e.message}")
209+
```
210+
211+
You can also catch the base class for all client validation exceptions:
212+
213+
```python
214+
from fitbit_client.exceptions import ClientValidationException
215+
216+
try:
217+
client.activity.create_activity_log(duration_millis=-100, start_time="12:00", date="2024-02-20")
218+
except ClientValidationException as e:
219+
print(f"Validation error: {e.message}")
220+
```
221+
173222
### ValidationException
174223

175224
Raised when input parameters do not meet requirements:
@@ -238,17 +287,49 @@ except RateLimitExceededException as e:
238287

239288
### Exception Properties
240289

241-
All exceptions provide these properties:
290+
API exceptions (`FitbitAPIException` and its subclasses) provide these
291+
properties:
242292

243293
- `message`: Human-readable error description
244294
- `status_code`: HTTP status code (if applicable)
245295
- `error_type`: Type of error from the API
246296
- `field_name`: Name of the invalid field (for validation errors)
247297

298+
Validation exceptions (`ClientValidationException` and its subclasses) provide:
299+
300+
- `message`: Human-readable error description
301+
- `field_name`: Name of the invalid field (for validation errors)
302+
303+
Specific validation exception subclasses provide additional properties:
304+
305+
- `InvalidDateException`: Adds `date_str` property with the invalid date string
306+
- `InvalidDateRangeException`: Adds `start_date`, `end_date`, `max_days`, and
307+
`resource_name` properties
308+
- `IntradayValidationException`: Adds `allowed_values` and `resource_name`
309+
properties
310+
- `ParameterValidationException`: Used for invalid parameter values (e.g.,
311+
negative where positive is required)
312+
- `MissingParameterException`: Used when required parameters are missing or
313+
parameter combinations are invalid
314+
248315
### Exception Hierarchy:
249316

250317
```
251318
Exception
319+
├── ValueError
320+
│ └── ClientValidationException # Superclass for validations that take place before
321+
│ │ # making a request
322+
│ ├── InvalidDateException # Raised when a date string is not in the correct
323+
│ │ # format or not a valid calendar date
324+
│ ├── InvalidDateRangeException # Raised when a date range is invalid (e.g., end is
325+
│ │ # before start, exceeds max days)
326+
│ ├── PaginationException # Raised when pagination parameters are invalid
327+
│ ├── IntradayValidationException # Raised when intraday request parameters are invalid
328+
│ ├── ParameterValidationException # Raised when a parameter value is invalid
329+
│ │ # (e.g., negative when positive required)
330+
│ └── MissingParameterException # Raised when required parameters are missing or
331+
│ # parameter combinations are invalid
332+
252333
└── FitbitAPIException # Base exception for all Fitbit API errors
253334
254335
├── OAuthException # Superclass for all authentication flow exceptions
@@ -257,23 +338,15 @@ Exception
257338
│ ├── InvalidTokenException # Raised when the OAuth token is invalid
258339
│ └── InvalidClientException # Raised when the client_id is invalid
259340
260-
├── RequestException # Superclass for all API request exceptions
261-
│ ├── InvalidRequestException # Raised when the request syntax is invalid
262-
│ ├── AuthorizationException # Raised when there are authorization-related errors
263-
│ ├── InsufficientPermissionsException # Raised when the application has insufficient permissions
264-
│ ├── InsufficientScopeException # Raised when the application is missing a required scope
265-
│ ├── NotFoundException # Raised when the requested resource does not exist
266-
│ ├── RateLimitExceededException # Raised when the application hits rate limiting quotas
267-
│ ├── SystemException # Raised when there is a system-level failure
268-
│ └── ValidationException # Raised when a request parameter is invalid or missing
269-
270-
└── ClientValidationException # Superclass for validations that take place before
271-
│ # making a request
272-
├── InvalidDateException # Raised when a date string is not in the correct
273-
│ # format or not a valid calendar date
274-
├── InvalidDateRangeException # Raised when a date range is invalid (e.g., end is
275-
│ # before start, exceeds max days)
276-
└── IntradayValidationException # Raised when intraday request parameters are invalid
341+
└── RequestException # Superclass for all API request exceptions
342+
├── InvalidRequestException # Raised when the request syntax is invalid
343+
├── AuthorizationException # Raised when there are authorization-related errors
344+
├── InsufficientPermissionsException # Raised when the application has insufficient permissions
345+
├── InsufficientScopeException # Raised when the application is missing a required scope
346+
├── NotFoundException # Raised when the requested resource does not exist
347+
├── RateLimitExceededException # Raised when the application hits rate limiting quotas
348+
├── SystemException # Raised when there is a system-level failure
349+
└── ValidationException # Raised when a request parameter is invalid or missing
277350
```
278351

279352
## Debugging

fitbit_client/auth/callback_server.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def __init__(self, redirect_uri: str) -> None:
5454
raise InvalidRequestException(
5555
message="Request to invalid domain: redirect_uri must use HTTPS protocol.",
5656
status_code=400,
57-
error_type="request",
57+
error_type="invalid_request",
5858
field_name="redirect_uri",
5959
)
6060

@@ -237,9 +237,10 @@ def wait_for_callback(self, timeout: int = 300) -> Optional[str]:
237237

238238
self.logger.error("Callback wait timed out")
239239
raise InvalidRequestException(
240-
message="OAuth callback timed out waiting for response",
240+
message=f"OAuth callback timed out after {timeout} seconds",
241241
status_code=400,
242242
error_type="invalid_request",
243+
field_name="oauth_callback",
243244
)
244245

245246
def stop(self) -> None:

0 commit comments

Comments
 (0)