Skip to content

Commit 0ff96f3

Browse files
authored
Add json output (#28)
1 parent 89dfbf0 commit 0ff96f3

10 files changed

Lines changed: 586 additions & 77 deletions

File tree

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,4 +161,8 @@ cython_debug/
161161
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
162162
#.idea/
163163

164-
*.json
164+
*.json
165+
.direnv
166+
.ruff_cache
167+
.vscode
168+
.envrc

AGENTS.md

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
# AGENTS.md — fitbit-cli
2+
3+
Guidance for agentic coding agents working in this repository.
4+
5+
---
6+
7+
## Project Overview
8+
9+
`fitbit-cli` is a Python command-line tool for fetching and displaying personal health data from the Fitbit API. It uses OAuth2 PKCE for authentication, the `requests` library for HTTP calls, and `rich` for terminal output.
10+
11+
- **Language:** Python 3.12+
12+
- **Entry point:** `fitbit_cli/main.py``main()`
13+
- **CLI installed as:** `fitbit-cli`
14+
- **Token storage:** `~/.fitbit/token.json`
15+
16+
---
17+
18+
## Repository Layout
19+
20+
```
21+
fitbit_cli/
22+
__init__.py # Package version (__version__ = "1.6.0")
23+
cli.py # argparse setup; date parsing utilities
24+
exceptions.py # FitbitInitError, FitbitAPIError
25+
fitbit_api.py # FitbitAPI class wrapping all Fitbit REST endpoints
26+
fitbit_setup.py # OAuth2 PKCE flow; token read/write/update
27+
formatter.py # rich-based display functions + JSON extraction; CONSOLE singleton
28+
output.py # Output modes: table_display, json_display, raw_json_display
29+
main.py # Entrypoint: wires CLI args → API calls → output mode
30+
tests/
31+
cli_test.py # unittest-based tests for date parsing logic
32+
pyproject.toml # Build system + tool configuration (black, isort, pylint, mypy)
33+
setup.py # Package metadata and runtime dependencies
34+
```
35+
36+
---
37+
38+
## Commands
39+
40+
### Setup
41+
```bash
42+
pip install -e .
43+
pip install black isort pylint mypy pytest pytest-cov
44+
```
45+
46+
### Tests
47+
```bash
48+
pytest tests/ # all tests
49+
pytest tests/cli_test.py # single file
50+
pytest tests/cli_test.py::TestCLIDateFunctions::test_get_date_range # single test
51+
python -m unittest tests.cli_test.TestCLIDateFunctions.test_get_date_range
52+
```
53+
54+
**Test file naming convention:** `*_test.py` (not `test_*.py`).
55+
56+
### Linting & formatting (run all before committing)
57+
```bash
58+
black fitbit_cli/ tests/
59+
isort fitbit_cli/ tests/
60+
pylint fitbit_cli/
61+
mypy fitbit_cli/
62+
```
63+
64+
### CI check (read-only)
65+
```bash
66+
black --check fitbit_cli/ tests/
67+
isort --check-only fitbit_cli/ tests/
68+
```
69+
70+
**Tool settings:** `black` line-length 88; `isort` profile `black`; `pylint` max-line-length 120, `E0401` disabled; `mypy` `ignore_missing_imports = true`. `flake8` and `ruff` are **not used**.
71+
72+
---
73+
74+
## Code Style Guidelines
75+
76+
### General Principles
77+
- Keep code **simple, short, and production-ready**.
78+
- Write as a senior Python developer — readable, direct, no overengineering.
79+
- Do not decompose into too many small functions for the sake of it.
80+
- **Do not change existing code** unless directly required by the task.
81+
82+
### File Header
83+
```python
84+
# -*- coding: utf-8 -*-
85+
"""
86+
Module Description
87+
"""
88+
```
89+
90+
### Imports
91+
- Order: stdlib → third-party (`requests`, `rich`) → relative
92+
- Relative symbol imports: `from .exceptions import FitbitAPIError`
93+
- Module-level alias imports: `from . import formatter as fmt`
94+
95+
### Naming Conventions
96+
97+
| Kind | Convention | Example |
98+
|------|------------|---------|
99+
| Classes | `PascalCase` | `FitbitAPI`, `FitbitInitError` |
100+
| Functions / methods | `snake_case` | `get_sleep_log`, `parse_date_range` |
101+
| Private helpers | `_leading_underscore` | `_create_headers`, `_get_date_range` |
102+
| Constants | `UPPER_SNAKE_CASE` | `BASE_URL`, `TOKEN_URL`, `CONSOLE` |
103+
| Variables | `snake_case` | `start_date`, `access_token` |
104+
105+
### Docstrings
106+
All public classes, methods, and functions must have a one-line docstring. No empty line after `def`.
107+
```python
108+
def get_sleep_log(self, start_date, end_date=None):
109+
"""Get Sleep Logs by Date Range and Date"""
110+
```
111+
112+
### Type Annotations
113+
Not currently used. Do not add unless refactoring a file end-to-end.
114+
115+
### String Formatting
116+
Use f-strings throughout. Never use `%`-formatting or `.format()`.
117+
118+
### Error Handling
119+
- Custom exceptions: `FitbitInitError`, `FitbitAPIError` in `exceptions.py`. Both accept a single `message` arg.
120+
- Use specific exception types; avoid bare `except:`.
121+
- Preserve tracebacks: `raise ... from e`.
122+
- HTTP 401 triggers automatic token refresh inside `make_request()`.
123+
124+
### HTTP Requests
125+
Always include `timeout=5`:
126+
```python
127+
response = requests.request(method, url, headers=self.headers, timeout=5, **kwargs)
128+
```
129+
130+
### Output
131+
- Table mode: always use `CONSOLE.print(...)`. Never call `print()` directly.
132+
- JSON mode: use `print(json.dumps(..., separators=(",", ":")))` for compact output. Never use `rich.print_json()` — it breaks on emoji characters in data.
133+
- Each `display_*` function in `formatter.py` accepts `as_json=False`. When `True`, returns a plain snake_case dict (no printing, no emoji keys). `output.py` collects dicts and prints once.
134+
- Both branches of every `display_*` function must return explicitly (pylint `R1710`).
135+
136+
```python
137+
def display_sleep(sleep_data, as_json=False):
138+
"""Sleep data formatter"""
139+
if as_json:
140+
return {"sleep": [...]}
141+
table = Table(...)
142+
CONSOLE.print(table)
143+
return None
144+
```
145+
146+
### pylint Inline Suppression
147+
Use sparingly and only when justified:
148+
```python
149+
# pylint: disable=C0301 # line too long
150+
# pylint: disable=C0413 # import not at top
151+
# pylint: disable=C0103 # invalid variable name
152+
```
153+
154+
---
155+
156+
## CLI Flags
157+
158+
| Flag | Short | Description |
159+
|------|-------|-------------|
160+
| `--init-auth` | `-i` | OAuth2 PKCE setup |
161+
| `--sleep` | `-s` | Sleep log |
162+
| `--spo2` | `-o` | SpO2 summary |
163+
| `--heart` | `-e` | Heart rate time series |
164+
| `--active-zone` | `-a` | Active zone minutes |
165+
| `--breathing-rate` | `-b` | Breathing rate summary |
166+
| `--activities` | `-t` | Daily activity summary |
167+
| `--user-profile` | `-u` | User profile |
168+
| `--devices` | `-d` | Devices list |
169+
| `--json` | `-j` | Output table data as JSON |
170+
| `--raw-json` | `-r` | Full raw JSON response from Fitbit API |
171+
| `--version` | `-v` | Show version |
172+
173+
`--json` and `--raw-json` suppress the spinner and output compact JSON to stdout — designed for AI agent use.
174+
175+
---
176+
177+
## Testing Conventions
178+
179+
- Framework: `unittest.TestCase`, discovered and run by pytest.
180+
- One test class per file, named `Test<Subject>`.
181+
- Each test method has a full docstring.
182+
- Use `unittest.mock.patch` to mock `datetime.today()` for deterministic date tests.
183+
- Add `sys.path.insert(0, ...)` at the top of test files when needed to resolve imports.
184+
185+
---
186+
187+
## CI/CD
188+
189+
- **ci.yml**: Runs on PRs. Executes `super-linter` (black + isort + pylint; flake8/ruff disabled) then `pytest --cov` on Python 3.12.
190+
- **release.yml**: Triggered on GitHub Release creation. Publishes to PyPI via `twine`.
191+
- **dependabot.yml**: Weekly updates for `pip` and `github-actions` dependencies.
192+
193+
---
194+
195+
## Runtime Notes
196+
197+
- OAuth2 PKCE runs a temporary local server on `127.0.0.1:8080` to receive the auth code.
198+
- Token file: `~/.fitbit/token.json` — contains `client_id`, `secret`, `access_token`, `refresh_token`.
199+
- Tokens are valid for 8 hours and auto-refreshed on 401 responses.
200+
- Only GET endpoints are implemented in `FitbitAPI`.

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
](https://pypi.org/project/fitbit-cli/) [![ClickPy Stats](https://img.shields.io/badge/ClickPy%20Stats-A5951E)
66
](https://clickpy.clickhouse.com/dashboard/fitbit-cli)
77

8-
> This is not an official Fitbit CLI
8+
> _This is not an official Fitbit CLI_
99
1010
Access your Fitbit data directly from your terminal 💻. View 💤 sleep logs, ❤️ heart rate, 🏋️‍♂️ activity levels, 🩸 SpO2, and more, all presented in a simple, easy-to-read table format!
1111

12+
> **AI agent-friendly** 🤖 — since v1.6.0, use `--json` for minimized, token-efficient JSON output or `--raw-json` for the full Fitbit API response. No spinners, pure JSON.
13+
1214
<p align="center">
1315
<img alt="Fitbit logo", width="350" src="https://raw.githubusercontent.com/veerendra2/fitbit-cli/refs/heads/main/assets/Fitbit_Logo_White_RGB.jpg">
1416
</p>
@@ -42,13 +44,16 @@ python -m pip install fitbit-cli
4244

4345
```bash
4446
fitbit-cli -h
45-
usage: fitbit-cli [-h] [-i] [-s [DATE[,DATE]|RELATIVE]] [-o [DATE[,DATE]|RELATIVE]] [-e [DATE[,DATE]|RELATIVE]] [-a [DATE[,DATE]|RELATIVE]] [-b [DATE[,DATE]|RELATIVE]] [-t [DATE[,DATE]|RELATIVE]] [-u] [-d] [-v]
47+
usage: fitbit-cli [-h] [-i] [-j] [-r] [-s [DATE[,DATE]|RELATIVE]] [-o [DATE[,DATE]|RELATIVE]] [-e [DATE[,DATE]|RELATIVE]] [-a [DATE[,DATE]|RELATIVE]]
48+
[-b [DATE[,DATE]|RELATIVE]] [-t [DATE[,DATE]|RELATIVE]] [-u] [-d] [-v]
4649

4750
Fitbit CLI -- Access your Fitbit data at your terminal.
4851

4952
options:
5053
-h, --help show this help message and exit
5154
-i, --init-auth Initialize Fitbit iterative authentication setup
55+
-j, --json Output table data as JSON.
56+
-r, --raw-json Output raw JSON from the Fitbit API.
5257
-v, --version Show fitbit-cli version
5358

5459
APIs:

fitbit_cli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
fitbit_cli Module
44
"""
55

6-
__version__ = "1.5.2"
6+
__version__ = "1.6.0"

fitbit_cli/cli.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,20 @@ def parse_arguments():
7272
help="Initialize Fitbit iterative authentication setup",
7373
)
7474

75+
parser.add_argument(
76+
"-j",
77+
"--json",
78+
action="store_true",
79+
help="Output table data as JSON.",
80+
)
81+
82+
parser.add_argument(
83+
"-r",
84+
"--raw-json",
85+
action="store_true",
86+
help="Output raw JSON from the Fitbit API.",
87+
)
88+
7589
group = parser.add_argument_group(
7690
"APIs",
7791
"Specify a date, date range (YYYY-MM-DD[,YYYY-MM-DD]), or relative date.\n"
@@ -155,7 +169,13 @@ def parse_arguments():
155169

156170
args = parser.parse_args()
157171

158-
if not any(vars(args).values()):
172+
data_args = {
173+
k: v
174+
for k, v in vars(args).items()
175+
if k not in ("json", "raw_json", "init_auth", "version")
176+
}
177+
178+
if not args.init_auth and not any(data_args.values()):
159179
parser.error("No arguments provided. At least one argument is required.")
160180

161181
return args

0 commit comments

Comments
 (0)