- Python 3.13+
- PDM
- Git
- Clone the repository:
git clone https://github.com/jpstroop/fitbit-client-python.git
cd fitbit-client-python- Install asdf plugins and required versions:
asdf plugin add python
asdf plugin add pdm
asdf install python 3.13.0
asdf install pdm latest
asdf local python 3.13.0
asdf local pdm latest- Install project dependencies:
pdm install -G:allfitbit-client/
├── fitbit_client/
│ ├── __init__.py
│ ├── auth/
│ │ ├── __init__.py
│ │ ├── callback_handler.py
│ │ ├── callback_server.py
│ │ └── oauth.py
│ ├── client.py
│ ├── resources/
│ │ ├── __init__.py
│ │ ├── [resource modules]
│ │ ├── base.py
│ │ └── constants.py
│ ├── utils/
│ │ ├── __init__.py
│ │ ├── curl_debug_mixin.py
│ │ ├── date_validation.py
│ │ ├── helpers.py
│ │ ├── pagination_validation.py
│ │ └── types.py
│ └── exceptions.py
├── tests/
│ ├── auth/
│ ├── resources/
│ └── utils/
└── [project files]
For now these are just in TODO.md; bigger work will eventually move to Github tickets.
- Black for code formatting (100 character line length)
- isort for import sorting
- Type hints required for all code
- Docstrings required for all public methods
Prefer importing specific names rather than entire modules, one import per line. Examples:
# Good
from os.path import join
from os.path import exists
from typing import Dict
from typing import List
from typing import Optional
from datetime import datetime
# Bad
from os.path import exists, join
from typing import Optional, Dict, List
import os
import typing
import datetime
The one exception to this rule is when an entire module needs to be mocked for
testing, in which case, at least for the json package from the standard
library, the entire package has to be imported. So import json is ok when that
circumstance arises.
pdm run formatFollow your nose from client.py and the structure should be very clear.
- Include comprehensive docstrings with Args sections
- Keep parameter naming consistent across methods
- Use "-" as default for user_id parameters
- Return Dict[str, Any] for most methods that return data
- Return None for delete operations
The codebase implements a comprehensive error handling system through
exceptions.py:
-
A base FitbitAPIException that captures:
- HTTP status code
- Error type
- Error message
- Field name (when applicable)
-
Specialized exceptions for different error scenarios:
- InvalidRequestException for malformed requests
- ValidationException for parameter validation failures
- AuthorizationException for authentication issues
- RateLimitExceededException for API throttling
- SystemException for server-side errors
-
Mapping from HTTP status codes and API error types to appropriate exception classes
- Only use enums for validating request parameters, not responses
- Place all enums in constants.py
- Only import enums that are actively used in the class
The project implements two different logs in through the
BaseResource class: application logging for
API interactions and data logging for tracking important response fields. See
LOGGING for details.
The primary API structure is resource-based, organizing related endpoints into dedicated resource classes based on the structure and organzation of the Fibit Web API itself, e.g.
client.user- User profile and badges endpointsclient.activity- Activity tracking, goals, and summariesclient.sleep- Sleep logs and goals- etc.
All resource methods are also available directly from the client instance through aliases. This means developers can choose between two equivalent approaches:
# Standard resource-based access
client.user.get_profile()
client.activity.get_daily_activity_summary(date="2025-03-06")
# Direct access via method aliases
client.get_profile()
client.get_daily_activity_summary(date="2025-03-06")Method aliases were implemented for several important reasons:
-
Reduced Verbosity: Typing
client.resource_name.method_name(...)with many parameters can be tedious, especially when used frequently. -
Flatter API Surface: Many modern APIs prefer a flatter design that avoids deep nesting, making the API more straightforward to use.
-
Method Name Uniqueness: All resource methods in the Fitbit API have unique names (e.g., there's only one
get_profile()method), making it safe to expose these methods directly on the client. -
Preserve Both Options: By maintaining both the resource-based access and direct aliases, developers can choose the approach that best fits their needs - organization or conciseness.
All method aliases are set up in the _setup_method_aliases() method in the
FitbitClient class, which is called during
initialization. Each alias is a direct reference to the corresponding resource
method, ensuring consistent behavior regardless of how the method is accessed.
The project uses pytest for testing and follows a consistent testing approach across all components.
The test directory mirrors the main package structure (except that the root is named "test" rather than "fitbit_client"), with corresponding test modules for each component:
- auth/: Tests for authentication and OAuth functionality
- client/: Tests for the main client implementation
- resources/: Tests for individual API resource implementations
The test suite provides several standard fixtures for use across test modules:
@fixture
def mock_oauth_session():
"""Provides a mock OAuth session for testing resources"""
return Mock()
@fixture
def mock_logger():
"""Provides a mock logger for testing logging behavior"""
return Mock()
@fixture
def base_resource(mock_oauth_session, mock_logger):
"""Creates a resource instance with mocked dependencies"""
with patch("fitbit_client.resources.base.getLogger", return_value=mock_logger):
return BaseResource(mock_oauth_session, "en_US", "en_US")Tests verify proper error handling across the codebase. Common patterns include:
def test_http_error_handling(resource):
"""Tests that HTTP errors are properly converted to exceptions"""
with raises(InvalidRequestException) as exc_info:
# Test code that should raise the exception
pass
assert exc_info.value.status_code == 400
assert exc_info.value.error_type == "validation"The test suite uses consistent patterns for mocking API responses:
mock_response = Mock()
mock_response.json.return_value = {"data": "test"}
mock_response.headers = {"content-type": "application/json"}
mock_response.status_code = 200The OAuth callback mechanism is implemented using two main classes:
CallbackServer and CallbackHandler.
CallbackServerstarts an HTTPS server on localhost with a dynamically generated SSL certificate- When the OAuth provider redirects to our callback URL,
CallbackHandlerreceives the GET request - The handler stores the full callback URL path (including query parameters) on
the server instance using
setattr(self.server, "last_callback", self.path) CallbackServer.wait_for_callback()polls for this stored path usinggetattr()until either:- The callback data is found (returns the URL path)
- The timeout is reached (returns None)
- When complete,
stop()cleans up by:- Shutting down the HTTP server
- Removing temporary certificate files
- Clearing internal state
- Create a new branch for your feature/fix
- Make your changes, following the style guidelines (see also: LINTING)
- Run formatting checks (
pdm format) and tests (pdm run pytest) - Submit a pull request with a clear description of changes
This section will be documented as we near our first release.
This client implements intraday data endpoints (detailed heart rate, steps, etc)
through the IntradayResource class. These endpoints have some special
requirements if you're using them for anyone other that yourself. See the
Intraday API documentation
for more details.