Skip to content

Commit 11420fd

Browse files
committed
bring back resource method aliases
1 parent 5ecc6e1 commit 11420fd

File tree

4 files changed

+343
-1
lines changed

4 files changed

+343
-1
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ try:
4848
client.authenticate()
4949

5050
# Make a request (e.g., get user profile)
51+
# You can access resources directly:
5152
profile = client.user.get_profile()
53+
# Or use method aliases for shorter syntax:
54+
profile = client.get_profile()
5255
print(dumps(profile, indent=2))
5356

5457
except Exception as e:
@@ -59,6 +62,29 @@ The response will always be the body of the API response, and is almost always a
5962
`Dict`, `List` or `None`. `nutrition.get_activity_tcx` is the exception. It
6063
returns XML (as a `str`).
6164

65+
## Method Aliases
66+
67+
All resource methods are available directly from the client instance. This means
68+
you can use:
69+
70+
```python
71+
# Short form with method aliases
72+
client.get_profile()
73+
client.get_daily_activity_summary(date="2025-03-06")
74+
client.get_sleep_log_by_date(date="2025-03-06")
75+
```
76+
77+
Instead of the longer form:
78+
79+
```python
80+
# Standard resource access
81+
client.user.get_profile()
82+
client.activity.get_daily_activity_summary(date="2025-03-06")
83+
client.sleep.get_sleep_log_by_date(date="2025-03-06")
84+
```
85+
86+
Both approaches are equivalent, but aliases provide a more concise syntax.
87+
6288
## Authentication Methods
6389

6490
### 1. Automatic (Recommended)

docs/DEVELOPMENT.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
- [Logging System](#logging-system)
1717
- [Application Logger](#application-logger)
1818
- [Data Logger](#data-logger)
19+
- [API Design](#api-design)
20+
- [Resource-Based API](#resource-based-api)
21+
- [Method Aliases](#method-aliases)
1922
- [Testing](#testing)
2023
- [Test Organization](#test-organization)
2124
- [Standard Test Fixtures](#standard-test-fixtures)
@@ -235,6 +238,63 @@ Data log entries contain:
235238
This logging system provides both operational visibility through the application
236239
logger and structured data capture through the data logger.
237240

241+
## API Design
242+
243+
The client implements a dual-level API design pattern that balances both
244+
organization and ease-of-use.
245+
246+
### Resource-Based API
247+
248+
The primary API structure is resource-based, organizing related endpoints into
249+
dedicated resource classes:
250+
251+
- `client.user` - User profile and badges endpoints
252+
- `client.activity` - Activity tracking, goals, and summaries
253+
- `client.sleep` - Sleep logs and goals
254+
- etc.
255+
256+
This organization provides a clean separation of concerns and makes the code
257+
more maintainable by grouping related functionality.
258+
259+
### Method Aliases
260+
261+
To improve developer experience, all resource methods are also available
262+
directly from the client instance through aliases. This means developers can
263+
choose between two equivalent approaches:
264+
265+
```python
266+
# Standard resource-based access
267+
client.user.get_profile()
268+
client.activity.get_daily_activity_summary(date="2025-03-06")
269+
270+
# Direct access via method aliases
271+
client.get_profile()
272+
client.get_daily_activity_summary(date="2025-03-06")
273+
```
274+
275+
#### Rationale for Method Aliases
276+
277+
Method aliases were implemented for several important reasons:
278+
279+
1. **Reduced Verbosity**: Typing `client.resource_name.method_name(...)` with
280+
many parameters can be tedious, especially when used frequently.
281+
282+
2. **Flatter API Surface**: Many modern APIs prefer a flatter design that avoids
283+
deep nesting, making the API more straightforward to use.
284+
285+
3. **Method Name Uniqueness**: All resource methods in the Fitbit API have
286+
unique names (e.g., there's only one `get_profile()` method), making it safe
287+
to expose these methods directly on the client.
288+
289+
4. **Preserve Both Options**: By maintaining both the resource-based access and
290+
direct aliases, developers can choose the approach that best fits their needs
291+
\- organization or conciseness.
292+
293+
All method aliases are set up in the `_setup_method_aliases()` method in the
294+
`FitbitClient` class, which is called during initialization. Each alias is a
295+
direct reference to the corresponding resource method, ensuring consistent
296+
behavior regardless of how the method is accessed.
297+
238298
## Testing
239299

240300
The project uses pytest for testing and follows a consistent testing approach

fitbit_client/client.py

Lines changed: 199 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ def __init__(
113113
# isort: on
114114
self.logger.debug("Fitbit client initialized successfully")
115115

116-
# API aliases will be re-implemented after resource methods have been refactored.
116+
# Set up method aliases
117+
self._setup_method_aliases()
117118

118119
def authenticate(self, force_new: bool = False) -> bool:
119120
"""
@@ -145,3 +146,200 @@ def authenticate(self, force_new: bool = False) -> bool:
145146
except SystemException as e:
146147
self.logger.error(f"System error during authentication: {str(e)}")
147148
raise
149+
150+
def _setup_method_aliases(self) -> None:
151+
"""Set up direct access to resource methods as client attributes for convenience."""
152+
self.logger.debug("Setting up method aliases")
153+
154+
# Active Zone Minutes
155+
self.get_azm_timeseries_by_date = self.active_zone_minutes.get_azm_timeseries_by_date
156+
self.get_azm_timeseries_by_interval = (
157+
self.active_zone_minutes.get_azm_timeseries_by_interval
158+
)
159+
160+
# Activity Timeseries
161+
self.get_activity_timeseries_by_date = (
162+
self.activity_timeseries.get_activity_timeseries_by_date
163+
)
164+
self.get_activity_timeseries_by_date_range = (
165+
self.activity_timeseries.get_activity_timeseries_by_date_range
166+
)
167+
168+
# Activity
169+
self.create_activity_goals = self.activity.create_activity_goals
170+
self.create_activity_goal = self.activity.create_activity_goal
171+
self.create_activity_log = self.activity.create_activity_log
172+
self.get_activity_log_list = self.activity.get_activity_log_list
173+
self.create_favorite_activity = self.activity.create_favorite_activity
174+
self.delete_activity_log = self.activity.delete_activity_log
175+
self.delete_favorite_activity = self.activity.delete_favorite_activity
176+
self.get_activity_goals = self.activity.get_activity_goals
177+
self.get_daily_activity_summary = self.activity.get_daily_activity_summary
178+
self.get_activity_type = self.activity.get_activity_type
179+
self.get_all_activity_types = self.activity.get_all_activity_types
180+
self.get_favorite_activities = self.activity.get_favorite_activities
181+
self.get_frequent_activities = self.activity.get_frequent_activities
182+
self.get_recent_activity_types = self.activity.get_recent_activity_types
183+
self.get_lifetime_stats = self.activity.get_lifetime_stats
184+
self.get_activity_tcx = self.activity.get_activity_tcx
185+
186+
# Body Timeseries
187+
self.get_body_timeseries_by_date = self.body_timeseries.get_body_timeseries_by_date
188+
self.get_body_timeseries_by_date_range = (
189+
self.body_timeseries.get_body_timeseries_by_date_range
190+
)
191+
self.get_bodyfat_timeseries_by_date = self.body_timeseries.get_bodyfat_timeseries_by_date
192+
self.get_bodyfat_timeseries_by_date_range = (
193+
self.body_timeseries.get_bodyfat_timeseries_by_date_range
194+
)
195+
self.get_weight_timeseries_by_date = self.body_timeseries.get_weight_timeseries_by_date
196+
self.get_weight_timeseries_by_date_range = (
197+
self.body_timeseries.get_weight_timeseries_by_date_range
198+
)
199+
200+
# Body
201+
self.create_bodyfat_goal = self.body.create_bodyfat_goal
202+
self.create_bodyfat_log = self.body.create_bodyfat_log
203+
self.create_weight_goal = self.body.create_weight_goal
204+
self.create_weight_log = self.body.create_weight_log
205+
self.delete_bodyfat_log = self.body.delete_bodyfat_log
206+
self.delete_weight_log = self.body.delete_weight_log
207+
self.get_body_goals = self.body.get_body_goals
208+
self.get_bodyfat_log = self.body.get_bodyfat_log
209+
self.get_weight_logs = self.body.get_weight_logs
210+
211+
# Breathing Rate
212+
self.get_breathing_rate_summary_by_date = (
213+
self.breathing_rate.get_breathing_rate_summary_by_date
214+
)
215+
self.get_breathing_rate_summary_by_interval = (
216+
self.breathing_rate.get_breathing_rate_summary_by_interval
217+
)
218+
219+
# Cardio Fitness Score
220+
self.get_vo2_max_summary_by_date = self.cardio_fitness_score.get_vo2_max_summary_by_date
221+
self.get_vo2_max_summary_by_interval = (
222+
self.cardio_fitness_score.get_vo2_max_summary_by_interval
223+
)
224+
225+
# Device
226+
self.get_devices = self.device.get_devices
227+
228+
# Electrocardiogram
229+
self.get_ecg_log_list = self.electrocardiogram.get_ecg_log_list
230+
231+
# Friends
232+
self.get_friends = self.friends.get_friends
233+
self.get_friends_leaderboard = self.friends.get_friends_leaderboard
234+
235+
# Heartrate Timeseries
236+
self.get_heartrate_timeseries_by_date = (
237+
self.heartrate_timeseries.get_heartrate_timeseries_by_date
238+
)
239+
self.get_heartrate_timeseries_by_date_range = (
240+
self.heartrate_timeseries.get_heartrate_timeseries_by_date_range
241+
)
242+
243+
# Heartrate Variability
244+
self.get_hrv_summary_by_date = self.heartrate_variability.get_hrv_summary_by_date
245+
self.get_hrv_summary_by_interval = self.heartrate_variability.get_hrv_summary_by_interval
246+
247+
# Intraday
248+
self.get_azm_intraday_by_date = self.intraday.get_azm_intraday_by_date
249+
self.get_azm_intraday_by_interval = self.intraday.get_azm_intraday_by_interval
250+
self.get_activity_intraday_by_date = self.intraday.get_activity_intraday_by_date
251+
self.get_activity_intraday_by_interval = self.intraday.get_activity_intraday_by_interval
252+
self.get_breathing_rate_intraday_by_date = self.intraday.get_breathing_rate_intraday_by_date
253+
self.get_breathing_rate_intraday_by_interval = (
254+
self.intraday.get_breathing_rate_intraday_by_interval
255+
)
256+
self.get_heartrate_intraday_by_date = self.intraday.get_heartrate_intraday_by_date
257+
self.get_heartrate_intraday_by_interval = self.intraday.get_heartrate_intraday_by_interval
258+
self.get_hrv_intraday_by_date = self.intraday.get_hrv_intraday_by_date
259+
self.get_hrv_intraday_by_interval = self.intraday.get_hrv_intraday_by_interval
260+
self.get_spo2_intraday_by_date = self.intraday.get_spo2_intraday_by_date
261+
self.get_spo2_intraday_by_interval = self.intraday.get_spo2_intraday_by_interval
262+
263+
# Irregular Rhythm Notifications
264+
self.get_irn_alerts_list = self.irregular_rhythm_notifications.get_irn_alerts_list
265+
self.get_irn_profile = self.irregular_rhythm_notifications.get_irn_profile
266+
267+
# Nutrition Timeseries
268+
self.get_nutrition_timeseries_by_date = (
269+
self.nutrition_timeseries.get_nutrition_timeseries_by_date
270+
)
271+
self.get_nutrition_timeseries_by_date_range = (
272+
self.nutrition_timeseries.get_nutrition_timeseries_by_date_range
273+
)
274+
275+
# Nutrition
276+
self.add_favorite_foods = self.nutrition.add_favorite_foods
277+
self.add_favorite_food = self.nutrition.add_favorite_food
278+
self.create_favorite_food = self.nutrition.create_favorite_food
279+
self.create_food = self.nutrition.create_food
280+
self.create_food_log = self.nutrition.create_food_log
281+
self.create_food_goal = self.nutrition.create_food_goal
282+
self.create_meal = self.nutrition.create_meal
283+
self.create_water_goal = self.nutrition.create_water_goal
284+
self.create_water_log = self.nutrition.create_water_log
285+
self.delete_custom_food = self.nutrition.delete_custom_food
286+
self.delete_favorite_foods = self.nutrition.delete_favorite_foods
287+
self.delete_favorite_food = self.nutrition.delete_favorite_food
288+
self.delete_food_log = self.nutrition.delete_food_log
289+
self.delete_meal = self.nutrition.delete_meal
290+
self.delete_water_log = self.nutrition.delete_water_log
291+
self.get_food = self.nutrition.get_food
292+
self.get_food_goals = self.nutrition.get_food_goals
293+
self.get_food_log = self.nutrition.get_food_log
294+
self.get_food_locales = self.nutrition.get_food_locales
295+
self.get_food_units = self.nutrition.get_food_units
296+
self.get_frequent_foods = self.nutrition.get_frequent_foods
297+
self.get_recent_foods = self.nutrition.get_recent_foods
298+
self.get_favorite_foods = self.nutrition.get_favorite_foods
299+
self.get_meal = self.nutrition.get_meal
300+
self.get_meals = self.nutrition.get_meals
301+
self.get_water_goal = self.nutrition.get_water_goal
302+
self.get_water_log = self.nutrition.get_water_log
303+
self.search_foods = self.nutrition.search_foods
304+
self.update_food_log = self.nutrition.update_food_log
305+
self.update_meal = self.nutrition.update_meal
306+
self.update_water_log = self.nutrition.update_water_log
307+
308+
# Sleep
309+
self.create_sleep_goals = self.sleep.create_sleep_goals
310+
self.create_sleep_goal = self.sleep.create_sleep_goal
311+
self.create_sleep_log = self.sleep.create_sleep_log
312+
self.delete_sleep_log = self.sleep.delete_sleep_log
313+
self.get_sleep_goals = self.sleep.get_sleep_goals
314+
self.get_sleep_goal = self.sleep.get_sleep_goal
315+
self.get_sleep_log_by_date = self.sleep.get_sleep_log_by_date
316+
self.get_sleep_log_by_date_range = self.sleep.get_sleep_log_by_date_range
317+
self.get_sleep_log_list = self.sleep.get_sleep_log_list
318+
319+
# SpO2
320+
self.get_spo2_summary_by_date = self.spo2.get_spo2_summary_by_date
321+
self.get_spo2_summary_by_interval = self.spo2.get_spo2_summary_by_interval
322+
323+
# Subscription
324+
self.get_subscription_list = self.subscription.get_subscription_list
325+
326+
# Temperature
327+
self.get_temperature_core_summary_by_date = (
328+
self.temperature.get_temperature_core_summary_by_date
329+
)
330+
self.get_temperature_core_summary_by_interval = (
331+
self.temperature.get_temperature_core_summary_by_interval
332+
)
333+
self.get_temperature_skin_summary_by_date = (
334+
self.temperature.get_temperature_skin_summary_by_date
335+
)
336+
self.get_temperature_skin_summary_by_interval = (
337+
self.temperature.get_temperature_skin_summary_by_interval
338+
)
339+
340+
# User
341+
self.get_profile = self.user.get_profile
342+
self.update_profile = self.user.update_profile
343+
self.get_badges = self.user.get_badges
344+
345+
self.logger.debug("Method aliases set up successfully")

tests/test_method_aliases.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# tests/test_method_aliases.py
2+
3+
# Third party imports
4+
import pytest
5+
6+
# Local imports
7+
from fitbit_client.client import FitbitClient
8+
9+
10+
class TestMethodAliases:
11+
"""Test that all resource methods are properly aliased in the client."""
12+
13+
def test_method_aliases_implementation(self):
14+
"""Verify that the client has set up method aliases for all resources."""
15+
# Check that the client file has the required method and call
16+
with open(
17+
"/Users/jstroop/workspace/fitbit-client-python/fitbit_client/client.py", "r"
18+
) as f:
19+
client_content = f.read()
20+
21+
# Check that the _setup_method_aliases method exists
22+
assert (
23+
"def _setup_method_aliases" in client_content
24+
), "The _setup_method_aliases method is missing"
25+
26+
# Check that it's called in __init__
27+
assert (
28+
"self._setup_method_aliases()" in client_content
29+
), "_setup_method_aliases() is not called in __init__"
30+
31+
# Check that there are assignments for all resources
32+
resources = [
33+
"active_zone_minutes",
34+
"activity",
35+
"activity_timeseries",
36+
"body",
37+
"body_timeseries",
38+
"breathing_rate",
39+
"cardio_fitness_score",
40+
"device",
41+
"electrocardiogram",
42+
"friends",
43+
"heartrate_timeseries",
44+
"heartrate_variability",
45+
"intraday",
46+
"irregular_rhythm_notifications",
47+
"nutrition",
48+
"nutrition_timeseries",
49+
"sleep",
50+
"spo2",
51+
"subscription",
52+
"temperature",
53+
"user",
54+
]
55+
56+
for resource in resources:
57+
# Check at least one method from each resource is aliased
58+
assert f"self.{resource}." in client_content, f"No method aliases found for {resource}"

0 commit comments

Comments
 (0)