Skip to content

Commit 2274887

Browse files
committed
Fix mypy typing issues and achieve 100% test coverage for pagination
- Add TYPE_CHECKING for proper handling of circular imports - Fix return type annotations in BaseResource - Remove unreachable code in BaseResource methods - Add comprehensive tests for pagination edge cases - Move imports to top of files following best practices - Update pagination documentation with implementation details - Configure test coverage to exclude TYPE_CHECKING blocks
1 parent edfe8df commit 2274887

File tree

16 files changed

+529
-276
lines changed

16 files changed

+529
-276
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

docs/PAGINATION.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Some Fitbit API endpoints return potentially large result sets and support
44
pagination. This library provides an easy way to work with these paginated
5-
endpoints.
5+
endpoints through a robust and type-safe implementation.
66

77
## Supported Endpoints
88

@@ -87,3 +87,23 @@ Each paginated endpoint has specific constraints:
8787

8888
- Max limit: 10 entries per page
8989
- Only supports `offset=0`
90+
91+
## Implementation Details
92+
93+
The pagination implementation uses the following approach:
94+
95+
### Pagination Iterator
96+
97+
- Uses the `PaginatedIterator` class that implements the Python `Iterator` protocol
98+
- Automatically handles fetching the next page when needed using the `next` URL from pagination metadata
99+
- Properly handles edge cases like invalid responses, missing pagination data, and API errors
100+
101+
### Type Safety
102+
103+
- Uses `TYPE_CHECKING` from the typing module to avoid circular imports at runtime
104+
- Maintains complete type safety and mypy compatibility
105+
- All pagination-related code has 100% test coverage
106+
107+
### Resource Integration
108+
109+
Each endpoint that supports pagination has an `as_iterator` parameter that, when set to `True`, returns a `PaginatedIterator` instead of the raw API response. This makes it easy to iterate through all pages of results without manually handling pagination.

fitbit_client/resources/activity.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import Dict
66
from typing import Never
77
from typing import Optional
8+
from typing import TYPE_CHECKING
89
from typing import Union
910
from typing import cast
1011

@@ -15,12 +16,21 @@
1516
from fitbit_client.resources.constants import ActivityGoalPeriod
1617
from fitbit_client.resources.constants import ActivityGoalType
1718
from fitbit_client.resources.constants import SortDirection
19+
from fitbit_client.resources.pagination import create_paginated_iterator
1820
from fitbit_client.utils.date_validation import validate_date_param
1921
from fitbit_client.utils.pagination_validation import validate_pagination_params
2022
from fitbit_client.utils.types import JSONDict
2123
from fitbit_client.utils.types import JSONList
2224
from fitbit_client.utils.types import ParamDict
2325

26+
# We use TYPE_CHECKING to avoid circular imports at runtime.
27+
# PaginatedIterator is only needed for type annotations, not for runtime code.
28+
# This pattern is recommended by the Python typing documentation:
29+
# https://docs.python.org/3/library/typing.html#typing.TYPE_CHECKING
30+
if TYPE_CHECKING:
31+
# Local imports - only imported during type checking
32+
from fitbit_client.resources.pagination import PaginatedIterator
33+
2434

2535
class ActivityResource(BaseResource):
2636
"""Provides access to Fitbit Activity API for managing user activities and goals.
@@ -253,9 +263,7 @@ def get_activity_log_list(
253263
params["afterDate"] = after_date
254264

255265
endpoint = "activities/list.json"
256-
result = self._make_request(
257-
endpoint, params=params, user_id=user_id, debug=debug
258-
)
266+
result = self._make_request(endpoint, params=params, user_id=user_id, debug=debug)
259267

260268
# If debug mode is enabled, result will be None
261269
if debug or result is None:
@@ -264,15 +272,12 @@ def get_activity_log_list(
264272
# Return as iterator if requested
265273
# We use string literal type annotation 'PaginatedIterator' to avoid circular imports
266274
if as_iterator:
267-
# Local imports
268-
from fitbit_client.resources.pagination import create_paginated_iterator
269-
270275
return create_paginated_iterator(
271-
response=cast(JSONDict, result),
272-
resource=self,
276+
response=cast(JSONDict, result),
277+
resource=self,
273278
endpoint=endpoint,
274279
method_params=params,
275-
debug=debug
280+
debug=debug,
276281
)
277282

278283
return cast(JSONDict, result)

fitbit_client/resources/base.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -552,10 +552,7 @@ def _make_request(
552552
)
553553
raise
554554

555-
# If we successfully made a request and got a valid response, break the loop
556-
break
557-
558-
def _make_direct_request(self, path: str, debug: bool = False) -> JSONDict:
555+
def _make_direct_request(self, path: str, debug: bool = False) -> JSONType:
559556
"""Makes a request directly to the specified path.
560557
561558
This method is used internally for pagination to follow "next" URLs.
@@ -639,6 +636,3 @@ def _make_direct_request(self, path: str, debug: bool = False) -> JSONDict:
639636
error_type="request",
640637
status_code=500,
641638
)
642-
643-
# If we successfully made a request and got a valid response, break the loop
644-
break

fitbit_client/resources/electrocardiogram.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,23 @@
44
from typing import Any
55
from typing import Dict
66
from typing import Optional
7+
from typing import TYPE_CHECKING
78
from typing import Union
89
from typing import cast
910

1011
# Local imports
1112
from fitbit_client.resources.base import BaseResource
1213
from fitbit_client.resources.constants import SortDirection
14+
from fitbit_client.resources.pagination import create_paginated_iterator
1315
from fitbit_client.utils.date_validation import validate_date_param
1416
from fitbit_client.utils.pagination_validation import validate_pagination_params
1517
from fitbit_client.utils.types import JSONDict
1618
from fitbit_client.utils.types import ParamDict
1719

20+
if TYPE_CHECKING:
21+
# Local imports
22+
from fitbit_client.resources.pagination import PaginatedIterator
23+
1824

1925
class ElectrocardiogramResource(BaseResource):
2026
"""Provides access to Fitbit Electrocardiogram (ECG) API for retrieving heart rhythm assessments.
@@ -122,17 +128,14 @@ def get_ecg_log_list(
122128
return cast(JSONDict, result)
123129

124130
# Return as iterator if requested
125-
# We use string literal type annotation 'PaginatedIterator' to avoid circular imports
131+
# We use TYPE_CHECKING for PaginatedIterator type to avoid circular imports
126132
if as_iterator:
127-
# Local imports
128-
from fitbit_client.resources.pagination import create_paginated_iterator
129-
130133
return create_paginated_iterator(
131-
response=cast(JSONDict, result),
132-
resource=self,
134+
response=cast(JSONDict, result),
135+
resource=self,
133136
endpoint=endpoint,
134137
method_params=params,
135-
debug=debug
138+
debug=debug,
136139
)
137140

138141
return cast(JSONDict, result)

fitbit_client/resources/irregular_rhythm_notifications.py

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

33
# Standard library imports
44
from typing import Optional
5+
from typing import TYPE_CHECKING
56
from typing import Union
67
from typing import cast
78

89
# Local imports
910
from fitbit_client.resources.base import BaseResource
1011
from fitbit_client.resources.constants import SortDirection
12+
from fitbit_client.resources.pagination import create_paginated_iterator
1113
from fitbit_client.utils.date_validation import validate_date_param
1214
from fitbit_client.utils.pagination_validation import validate_pagination_params
1315
from fitbit_client.utils.types import JSONDict
1416
from fitbit_client.utils.types import ParamDict
1517

18+
if TYPE_CHECKING:
19+
# Local imports
20+
from fitbit_client.resources.pagination import PaginatedIterator
21+
1622

1723
class IrregularRhythmNotificationsResource(BaseResource):
1824
"""Provides access to Fitbit Irregular Rhythm Notifications (IRN) API for heart rhythm monitoring.
@@ -116,26 +122,21 @@ def get_irn_alerts_list(
116122
params["afterDate"] = after_date
117123

118124
endpoint = "irn/alerts/list.json"
119-
result = self._make_request(
120-
endpoint, params=params, user_id=user_id, debug=debug
121-
)
125+
result = self._make_request(endpoint, params=params, user_id=user_id, debug=debug)
122126

123127
# If debug mode is enabled, result will be None
124128
if debug or result is None:
125129
return cast(JSONDict, result)
126130

127131
# Return as iterator if requested
128-
# We use string literal type annotation 'PaginatedIterator' to avoid circular imports
132+
# We use TYPE_CHECKING for PaginatedIterator type to avoid circular imports
129133
if as_iterator:
130-
# Local imports
131-
from fitbit_client.resources.pagination import create_paginated_iterator
132-
133134
return create_paginated_iterator(
134-
response=cast(JSONDict, result),
135-
resource=self,
135+
response=cast(JSONDict, result),
136+
resource=self,
136137
endpoint=endpoint,
137138
method_params=params,
138-
debug=debug
139+
debug=debug,
139140
)
140141

141142
return cast(JSONDict, result)

0 commit comments

Comments
 (0)