Skip to content

Commit 5bd293b

Browse files
committed
Improve type safety with specialized dictionary types
Replace generic Optional[Dict[str, Any]] with specialized type aliases throughout the codebase: 1. ParamDict for query string parameters 2. FormDataDict for form data parameters 3. JSONDict for JSON data structures These changes provide better static type checking, improve code readability and maintainability, and fix all mypy type errors related to dictionary types.
1 parent f0df24c commit 5bd293b

File tree

14 files changed

+81
-42
lines changed

14 files changed

+81
-42
lines changed

TODO.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
- Typing
66

7-
- Try to get rid of `Optional[Dict[str, Any]]` args
7+
- ✅ Try to get rid of `Optional[Dict[str, Any]]` args - replaced with
8+
ParamDict, FormDataDict, JSONDict for more type safety
89

910
- base.py: reorganize and see what we can move out.
1011

fitbit_client/exceptions.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
from typing import List
77
from typing import Optional
88

9+
# Local imports
10+
from fitbit_client.utils.types import JSONDict
11+
912

1013
class FitbitAPIException(Exception):
1114
"""Base exception for all Fitbit API errors"""
@@ -15,7 +18,7 @@ def __init__(
1518
message: str,
1619
error_type: str,
1720
status_code: Optional[int] = None,
18-
raw_response: Optional[Dict[str, Any]] = None,
21+
raw_response: Optional[JSONDict] = None,
1922
field_name: Optional[str] = None,
2023
):
2124
self.message = message

fitbit_client/resources/activity.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from fitbit_client.utils.pagination_validation import validate_pagination_params
1919
from fitbit_client.utils.types import JSONDict
2020
from fitbit_client.utils.types import JSONList
21+
from fitbit_client.utils.types import ParamDict
2122

2223

2324
class ActivityResource(BaseResource):
@@ -88,7 +89,7 @@ def create_activity_goals(
8889
field_name="value",
8990
)
9091

91-
params = {"type": type.value, "value": value}
92+
params: ParamDict = {"type": type.value, "value": value}
9293
result = self._make_request(
9394
f"activities/goals/{period.value}.json",
9495
params=params,
@@ -152,24 +153,26 @@ def create_activity_log(
152153
- Start time should be in 24-hour format (e.g., "14:30" for 2:30 PM)
153154
"""
154155
if activity_id:
155-
params = {
156+
activity_params: ParamDict = {
156157
"activityId": activity_id,
157158
"startTime": start_time,
158159
"durationMillis": duration_millis,
159160
"date": date,
160161
}
161162
if distance is not None:
162-
params["distance"] = distance
163+
activity_params["distance"] = distance
163164
if distance_unit:
164-
params["distanceUnit"] = distance_unit
165+
activity_params["distanceUnit"] = distance_unit
166+
params = activity_params
165167
elif activity_name and manual_calories:
166-
params = {
168+
name_params: ParamDict = {
167169
"activityName": activity_name,
168170
"manualCalories": manual_calories,
169171
"startTime": start_time,
170172
"durationMillis": duration_millis,
171173
"date": date,
172174
}
175+
params = name_params
173176
else:
174177
raise MissingParameterException(
175178
message="Must provide either activity_id or (activity_name and manual_calories)",
@@ -229,7 +232,7 @@ def get_activity_log_list(
229232
- The source field indicates whether the activity was logged manually by the user
230233
or automatically by a Fitbit device
231234
"""
232-
params = {"sort": sort.value, "limit": limit, "offset": offset}
235+
params: ParamDict = {"sort": sort.value, "limit": limit, "offset": offset}
233236
if before_date:
234237
params["beforeDate"] = before_date
235238
if after_date:
@@ -651,7 +654,9 @@ def get_activity_tcx(
651654
- Not all activities have TCX data available (e.g., manually logged activities)
652655
- To check if an activity has GPS data, look for hasGps=True in the activity log
653656
"""
654-
params = {"includePartialTCX": include_partial_tcx} if include_partial_tcx else None
657+
params: Optional[ParamDict] = (
658+
{"includePartialTCX": include_partial_tcx} if include_partial_tcx else None
659+
)
655660
result = self._make_request(
656661
f"activities/{log_id}.tcx", params=params, user_id=user_id, debug=debug
657662
)

fitbit_client/resources/base.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from fitbit_client.exceptions import FitbitAPIException
2424
from fitbit_client.exceptions import STATUS_CODE_EXCEPTIONS
2525
from fitbit_client.utils.curl_debug_mixin import CurlDebugMixin
26+
from fitbit_client.utils.types import FormDataDict
2627
from fitbit_client.utils.types import JSONDict
2728
from fitbit_client.utils.types import JSONList
2829
from fitbit_client.utils.types import JSONType
@@ -139,7 +140,7 @@ def _build_url(
139140
return f"{self.API_BASE}/{api_version}/user/{user_id}/{endpoint}"
140141
return f"{self.API_BASE}/{api_version}/{endpoint}"
141142

142-
def _extract_important_fields(self, data: Dict[str, JSONType]) -> Dict[str, int | str]:
143+
def _extract_important_fields(self, data: JSONDict) -> Dict[str, JSONType]:
143144
"""
144145
Extract important fields from response data for logging.
145146
@@ -155,7 +156,7 @@ def _extract_important_fields(self, data: Dict[str, JSONType]) -> Dict[str, int
155156
"""
156157
extracted = {}
157158

158-
def extract_recursive(d: Dict[str, Any], prefix: str = "") -> None:
159+
def extract_recursive(d: JSONDict, prefix: str = "") -> None:
159160
for key, value in d.items():
160161
full_key = f"{prefix}.{key}" if prefix else key
161162

@@ -333,10 +334,10 @@ def _handle_error_response(self, response: Response) -> None:
333334
def _make_request(
334335
self,
335336
endpoint: str,
336-
data: Optional[Dict[str, Any]] = None,
337-
json: Optional[Dict[str, Any]] = None,
338-
params: Optional[Dict[str, Any]] = None,
339-
headers: Dict[str, Any] = {},
337+
data: Optional[FormDataDict] = None,
338+
json: Optional[JSONDict] = None,
339+
params: Optional[ParamDict] = None,
340+
headers: Dict[str, str] = {},
340341
user_id: str = "-",
341342
requires_user_id: bool = True,
342343
http_method: str = "GET",
@@ -363,7 +364,7 @@ def _make_request(
363364
364365
Returns:
365366
JSONType: The API response in one of these formats:
366-
- Dict[str, Any]: For most JSON object responses
367+
- JSONDict: For most JSON object responses
367368
- List[Any]: For endpoints that return JSON arrays
368369
- str: For XML/TCX responses
369370
- None: For successful DELETE operations or debug mode

fitbit_client/resources/body.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from fitbit_client.resources.constants import BodyGoalType
1010
from fitbit_client.utils.date_validation import validate_date_param
1111
from fitbit_client.utils.types import JSONDict
12+
from fitbit_client.utils.types import ParamDict
1213

1314

1415
class BodyResource(BaseResource):
@@ -101,7 +102,7 @@ def create_bodyfat_log(
101102
The 'source' field will be set to "API" for entries created through this endpoint.
102103
Multiple entries can be logged for the same day with different timestamps.
103104
"""
104-
params = {"fat": fat, "date": date}
105+
params: ParamDict = {"fat": fat, "date": date}
105106
if time:
106107
params["time"] = time
107108
result = self._make_request(
@@ -151,7 +152,7 @@ def create_weight_goal(
151152
- If target > start: "GAIN"
152153
- If target = start: "MAINTAIN"
153154
"""
154-
params = {"startDate": start_date, "startWeight": start_weight}
155+
params: ParamDict = {"startDate": start_date, "startWeight": start_weight}
155156
if weight is not None:
156157
params["weight"] = weight
157158
result = self._make_request(
@@ -208,7 +209,7 @@ def create_weight_log(
208209
The 'source' field will be set to "API" for entries created through this endpoint.
209210
Multiple weight entries can be logged for the same day with different timestamps.
210211
"""
211-
params = {"weight": weight, "date": date}
212+
params: ParamDict = {"weight": weight, "date": date}
212213
if time:
213214
params["time"] = time
214215
result = self._make_request(

fitbit_client/resources/electrocardiogram.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from fitbit_client.utils.date_validation import validate_date_param
1313
from fitbit_client.utils.pagination_validation import validate_pagination_params
1414
from fitbit_client.utils.types import JSONDict
15+
from fitbit_client.utils.types import ParamDict
1516

1617

1718
class ElectrocardiogramResource(BaseResource):
@@ -56,7 +57,7 @@ def get_ecg_log_list(
5657
offset: int = 0,
5758
user_id: str = "-",
5859
debug: bool = False,
59-
) -> Dict[str, Any]:
60+
) -> JSONDict:
6061
"""Returns a list of user's ECG log entries before or after a given day.
6162
6263
API Reference: https://dev.fitbit.com/build/reference/web-api/electrocardiogram/get-ecg-log-list/
@@ -92,7 +93,7 @@ def get_ecg_log_list(
9293
- resultClassification indicates the assessment outcome (normal, afib, inconclusive)
9394
- For research purposes only, not for clinical or diagnostic use
9495
"""
95-
params = {"sort": sort.value, "limit": limit, "offset": offset}
96+
params: ParamDict = {"sort": sort.value, "limit": limit, "offset": offset}
9697

9798
if before_date:
9899
params["beforeDate"] = before_date

fitbit_client/resources/heartrate_timeseries.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from fitbit_client.utils.date_validation import validate_date_param
1313
from fitbit_client.utils.date_validation import validate_date_range_params
1414
from fitbit_client.utils.types import JSONDict
15+
from fitbit_client.utils.types import ParamDict
1516

1617

1718
class HeartrateTimeSeriesResource(BaseResource):
@@ -105,7 +106,7 @@ def get_heartrate_timeseries_by_date(
105106
message="Only 'UTC' timezone is supported", field_name="timezone"
106107
)
107108

108-
params = {"timezone": timezone} if timezone else None
109+
params: Optional[ParamDict] = {"timezone": timezone} if timezone else None
109110
result = self._make_request(
110111
f"activities/heart/date/{date}/{period.value}.json",
111112
params=params,
@@ -169,7 +170,7 @@ def get_heartrate_timeseries_by_date_range(
169170
message="Only 'UTC' timezone is supported", field_name="timezone"
170171
)
171172

172-
params = {"timezone": timezone} if timezone else None
173+
params: Optional[ParamDict] = {"timezone": timezone} if timezone else None
173174
result = self._make_request(
174175
f"activities/heart/date/{start_date}/{end_date}.json",
175176
params=params,

fitbit_client/resources/irregular_rhythm_notifications.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from fitbit_client.utils.date_validation import validate_date_param
1111
from fitbit_client.utils.pagination_validation import validate_pagination_params
1212
from fitbit_client.utils.types import JSONDict
13+
from fitbit_client.utils.types import ParamDict
1314

1415

1516
class IrregularRhythmNotificationsResource(BaseResource):
@@ -93,7 +94,7 @@ def get_irn_alerts_list(
9394
- The alertTime is when the notification was generated, while detectedTime is
9495
when the irregular rhythm was detected (usually during sleep)
9596
"""
96-
params = {"sort": sort.value, "limit": limit, "offset": offset}
97+
params: ParamDict = {"sort": sort.value, "limit": limit, "offset": offset}
9798

9899
if before_date:
99100
params["beforeDate"] = before_date

fitbit_client/resources/nutrition.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -384,10 +384,15 @@ def create_meal(
384384
Food IDs can be obtained from food search results or the user's custom foods.
385385
"""
386386
# snakes to camels
387-
foods = [{to_camel_case(k): v for k, v in d.items()} for d in foods]
388-
data = {"name": name, "description": description, "mealFoods": foods}
387+
foods_list = [{to_camel_case(k): v for k, v in d.items()} for d in foods]
388+
# Use cast to handle the complex structure
389+
data_dict = {"name": name, "description": description, "mealFoods": foods_list}
389390
result = self._make_request(
390-
"meals.json", json=data, user_id=user_id, http_method="POST", debug=debug
391+
"meals.json",
392+
json=cast(JSONDict, data_dict),
393+
user_id=user_id,
394+
http_method="POST",
395+
debug=debug,
391396
)
392397
return cast(JSONDict, result)
393398

@@ -1179,10 +1184,15 @@ def update_meal(
11791184
Meal IDs can be obtained from the get_meals method. Updating a meal does not
11801185
affect any food logs that were previously created using this meal.
11811186
"""
1182-
foods = [{to_camel_case(k): v for k, v in d.items()} for d in foods]
1183-
data = {"name": name, "description": description, "mealFoods": foods}
1187+
foods_list = [{to_camel_case(k): v for k, v in d.items()} for d in foods]
1188+
# Use cast to handle the complex structure
1189+
data_dict = {"name": name, "description": description, "mealFoods": foods_list}
11841190
result = self._make_request(
1185-
f"meals/{meal_id}.json", json=data, user_id=user_id, http_method="POST", debug=debug
1191+
f"meals/{meal_id}.json",
1192+
json=cast(JSONDict, data_dict),
1193+
user_id=user_id,
1194+
http_method="POST",
1195+
debug=debug,
11861196
)
11871197
return cast(JSONDict, result)
11881198

fitbit_client/resources/sleep.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from fitbit_client.utils.date_validation import validate_date_range_params
1515
from fitbit_client.utils.pagination_validation import validate_pagination_params
1616
from fitbit_client.utils.types import JSONDict
17+
from fitbit_client.utils.types import ParamDict
1718

1819

1920
class SleepResource(BaseResource):
@@ -124,7 +125,7 @@ def create_sleep_log(
124125
message="duration_millis must be positive", field_name="duration_millis"
125126
)
126127

127-
params = {"startTime": start_time, "duration": duration_millis, "date": date}
128+
params: ParamDict = {"startTime": start_time, "duration": duration_millis, "date": date}
128129
result = self._make_request(
129130
"sleep.json",
130131
params=params,
@@ -355,7 +356,7 @@ def get_sleep_log_list(
355356
356357
This endpoint uses API version 1.2, unlike most other Fitbit API endpoints.
357358
"""
358-
params = {"sort": sort.value, "limit": limit, "offset": offset}
359+
params: ParamDict = {"sort": sort.value, "limit": limit, "offset": offset}
359360
if before_date:
360361
params["beforeDate"] = before_date
361362
if after_date:

0 commit comments

Comments
 (0)