commute-optimizer 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ben Kahan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,246 @@
1
+ Metadata-Version: 2.4
2
+ Name: commute-optimizer
3
+ Version: 1.1.0
4
+ Summary: Find the best time to leave: track real drive times via the Google Routes API and report the optimal departure window
5
+ Author: Ben Kahan
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/bkahan/commute-optimizer
8
+ Project-URL: Issues, https://github.com/bkahan/commute-optimizer/issues
9
+ Keywords: commute,traffic,google-maps,routes-api,departure-time
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: End Users/Desktop
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Dynamic: license-file
19
+
20
+ # Commute Optimizer
21
+
22
+ Tracks the real drive time between two places by polling the Google Routes API
23
+ over a week, then tells you the best time to leave — with far tighter resolution
24
+ than the wide ranges Google Maps shows.
25
+
26
+ No dependencies beyond Python 3.9+.
27
+
28
+ ```
29
+ pip install commute-optimizer
30
+ ```
31
+
32
+ That gives you the `commute` command used below. Running from a source
33
+ checkout works too — substitute `python -m commute` for `commute`.
34
+
35
+ All data files (`config.json`, `.env`, `commute.db`, the reports, logs) live
36
+ in the directory you run `commute` from, so each tracking project can have its
37
+ own folder.
38
+
39
+ ## Example output
40
+
41
+ ### The report
42
+
43
+ `commute report` writes a self-contained `report.html`: one section
44
+ per direction, a chart of median commute vs. departure time (one line per
45
+ weekday), and tables ranking the best departure slot inside each rush window —
46
+ including how many minutes it saves over the worst slot.
47
+
48
+ ![Example report with per-weekday commute charts and best-departure tables](docs/example-report.png)
49
+
50
+ *(Generated from synthetic demo data — regenerate with
51
+ `python docs/_gen_example.py`.)*
52
+
53
+ ### The terminal
54
+
55
+ Preflight check before a long run:
56
+
57
+ ```text
58
+ > commute check
59
+ [ok] config: '1600 Amphitheatre Parkway, Mountain View, CA' -> '1 Market St, San Francisco, CA'
60
+ [ok] API key found (AIzaSy...XXXX)
61
+ [ok] database writable at C:\...\commute-optimizer\commute.db (0 existing samples)
62
+ [ok] live route -> 43.6 min now (free-flow 46.3 min, 58.5 km)
63
+ [ok] live route <- 43.1 min now (free-flow 46.5 min, 58.9 km)
64
+ [ok] estimated API usage: ~938/day, ~6566/week (free tier ~10,000/month)
65
+
66
+ All checks passed - ready for a long run.
67
+ ```
68
+
69
+ Polling with the auto-stop timer:
70
+
71
+ ```text
72
+ > commute poll -d 7d
73
+ Polling '1600 Amphitheatre Parkway, Mountain View, CA' <-> '1 Market St, San Francisco, CA'
74
+ Rush interval 1 min, off-peak 10 min. Ctrl+C to stop.
75
+ Will stop automatically at 2026-07-12 22:59 and generate the report.
76
+ [Sun 22:59:28] -> 43.6 min (free-flow 46.3 min)
77
+ [Sun 22:59:28] <- 43.1 min (free-flow 46.5 min)
78
+ ...
79
+ ```
80
+
81
+ And when the report runs, it also prints the summary:
82
+
83
+ ```text
84
+ 1600 Amphitheatre Parkway, Mountain View, CA -> 1 Market St, San Francisco, CA
85
+ window 07:00-10:00:
86
+ Monday leave 09:45-10:00 (~28.7 min; worst 51.3 min, saves 22.6)
87
+ Tuesday leave 09:45-10:00 (~29.4 min; worst 51.5 min, saves 22.1)
88
+ ...
89
+ ```
90
+
91
+ ## Setup
92
+
93
+ 1. Get a Google Maps Platform API key with the **Routes API** enabled
94
+ (Google Cloud console → APIs & Services → enable "Routes API" → create an
95
+ API key).
96
+
97
+ 2. In the folder where you want the data to live, write the example config
98
+ and edit your addresses:
99
+
100
+ ```
101
+ commute init
102
+ ```
103
+
104
+ 3. Put your API key in a `.env` file in the same folder:
105
+
106
+ ```
107
+ GOOGLE_MAPS_API_KEY=your-key-here
108
+ ```
109
+
110
+ ## Usage
111
+
112
+ Before a long run, verify everything works (config, API key, database, one
113
+ live API call per direction, and a quota estimate for your poll schedule):
114
+
115
+ ```powershell
116
+ commute check
117
+ ```
118
+
119
+ Start polling. With `-d` it stops automatically after the given duration and
120
+ generates the report; without it, it runs until Ctrl+C:
121
+
122
+ ```powershell
123
+ commute poll -d 7d # a full week, then auto-report
124
+ commute poll # open-ended
125
+ ```
126
+
127
+ Durations accept `d`/`h`/`m` and combinations: `7d`, `24h`, `90m`, `1d12h`.
128
+ Add `--no-report` to skip the automatic report at the end.
129
+
130
+ Generate the report any time mid-run (opens `report.html` in your browser;
131
+ `--no-open` to skip that):
132
+
133
+ ```powershell
134
+ commute report
135
+ ```
136
+
137
+ Or watch the run continuously: this regenerates `report.html` *and* a
138
+ day-by-day report (`report-daily.html` — one curve per calendar date plus a
139
+ coverage table, so you can see the data filling in before the week is done).
140
+ Run it in a second terminal alongside `poll`; refresh the browser to see
141
+ updates:
142
+
143
+ ```powershell
144
+ commute watch # updates every 15 minutes
145
+ commute watch -i 5m # or your own interval
146
+ ```
147
+
148
+ To preview the report without waiting a week, seed a synthetic week of
149
+ two-direction data (refuses to run if the DB already has samples). In an
150
+ empty folder:
151
+
152
+ ```powershell
153
+ commute init
154
+ commute demo
155
+ commute report
156
+ ```
157
+
158
+ ### Surviving sleep and reboots
159
+
160
+ Instead of running `poll` in a terminal, register it with the OS so it stays
161
+ alive for the whole run (run this from your data folder — that's where the
162
+ scheduled poller will work):
163
+
164
+ ```powershell
165
+ commute schedule install -d 7d # poll for a week, survive anything
166
+ commute schedule status # is the task/poller alive?
167
+ commute schedule remove # stop and unregister
168
+ ```
169
+
170
+ This relaunches the poller every 5 minutes (a lock file makes that a no-op
171
+ while one is already running) and resumes after a reboot once you log in. The
172
+ stop time is stored as an absolute timestamp, so it survives reboots too;
173
+ when it's reached, the report is generated as usual.
174
+
175
+ Per platform:
176
+
177
+ - **Windows** — a Task Scheduler task; it can also *wake the machine from
178
+ sleep* to poll. Output goes to `poll.log` (no console window).
179
+ - **Linux** — a systemd user service + timer (`commute-poll`). Logs go to
180
+ `journalctl --user -u commute-poll`. Runs from login onward; for
181
+ before-login polling run `loginctl enable-linger $USER`. Linux can't wake
182
+ itself from sleep for this; a missed tick fires right after wake.
183
+ - **macOS** — a launchd LaunchAgent (`com.commute-optimizer.poll`) logging to
184
+ `poll.log`. macOS won't wake for it either — to poll with the lid closed,
185
+ plug in and run `caffeinate -s`.
186
+
187
+ Don't run `schedule install` while a manually-started `poll` from an older
188
+ version is still running — stop that one first, or you'll get duplicate
189
+ samples.
190
+
191
+ If you skip all this and the machine sleeps mid-run, collected data is still
192
+ safe in `commute.db` — the report's Coverage section will show exactly which
193
+ stretches were missed.
194
+
195
+ ## Config
196
+
197
+ ```json
198
+ {
199
+ "origin": "home address",
200
+ "destination": "work address",
201
+ "travel_mode": "DRIVE",
202
+ "track_both_directions": true,
203
+ "poll": {
204
+ "active_window": ["00:00", "23:59"],
205
+ "rush_windows": [["07:00", "10:00"], ["16:00", "19:00"]],
206
+ "rush_interval_min": 1,
207
+ "offpeak_interval_min": 10
208
+ }
209
+ }
210
+ ```
211
+
212
+ - `track_both_directions` — when true (the default), every tick samples both
213
+ origin→destination and the reverse, so one run covers your morning and
214
+ evening commutes.
215
+ - `active_window` — only poll inside this local-time range (set e.g.
216
+ `["06:00", "20:00"]` to skip the night entirely).
217
+ - `rush_windows` — during these windows the poller samples every
218
+ `rush_interval_min` minutes; otherwise every `offpeak_interval_min` minutes.
219
+ The report ranks departure times *within* these windows (otherwise 3 a.m.
220
+ always wins).
221
+
222
+ ## Cost
223
+
224
+ The Routes API (traffic-aware) has ~10,000 free calls/month. The default
225
+ adaptive schedule makes roughly 470 calls/day per direction — ~6,600/week with
226
+ both directions on, still inside the free tier but without room for a second
227
+ full week in the same month. Set `track_both_directions` to false or trim
228
+ `active_window` to halve it. Run `commute check` to see the estimate
229
+ for your exact schedule.
230
+
231
+ Samples land in `commute.db` (SQLite). Transient API failures (network blips,
232
+ rate limits, 5xx) are retried with backoff (5s/15s/45s) before being recorded
233
+ as an error row; permanent failures like a bad key fail fast without retries.
234
+ The report ends each direction with a **Coverage** section flagging any
235
+ stretch where sampling stopped (sleep, reboot, repeated errors) despite the
236
+ schedule expecting data — so you know which medians to trust.
237
+
238
+ ## Known limitations (v1)
239
+
240
+ - **Multi-week data blends together.** The report groups by weekday, so two
241
+ Mondays from different weeks merge into one median curve. Intended use is a
242
+ single week per `commute.db`; delete or rename the DB between runs.
243
+ - **Wake-from-sleep is Windows-only.** On Linux/macOS the scheduled poller
244
+ resumes right after wake but can't wake the machine itself.
245
+ - **No automated tests.** The pieces are small and hand-verified, but there's
246
+ no test suite yet.
@@ -0,0 +1,227 @@
1
+ # Commute Optimizer
2
+
3
+ Tracks the real drive time between two places by polling the Google Routes API
4
+ over a week, then tells you the best time to leave — with far tighter resolution
5
+ than the wide ranges Google Maps shows.
6
+
7
+ No dependencies beyond Python 3.9+.
8
+
9
+ ```
10
+ pip install commute-optimizer
11
+ ```
12
+
13
+ That gives you the `commute` command used below. Running from a source
14
+ checkout works too — substitute `python -m commute` for `commute`.
15
+
16
+ All data files (`config.json`, `.env`, `commute.db`, the reports, logs) live
17
+ in the directory you run `commute` from, so each tracking project can have its
18
+ own folder.
19
+
20
+ ## Example output
21
+
22
+ ### The report
23
+
24
+ `commute report` writes a self-contained `report.html`: one section
25
+ per direction, a chart of median commute vs. departure time (one line per
26
+ weekday), and tables ranking the best departure slot inside each rush window —
27
+ including how many minutes it saves over the worst slot.
28
+
29
+ ![Example report with per-weekday commute charts and best-departure tables](docs/example-report.png)
30
+
31
+ *(Generated from synthetic demo data — regenerate with
32
+ `python docs/_gen_example.py`.)*
33
+
34
+ ### The terminal
35
+
36
+ Preflight check before a long run:
37
+
38
+ ```text
39
+ > commute check
40
+ [ok] config: '1600 Amphitheatre Parkway, Mountain View, CA' -> '1 Market St, San Francisco, CA'
41
+ [ok] API key found (AIzaSy...XXXX)
42
+ [ok] database writable at C:\...\commute-optimizer\commute.db (0 existing samples)
43
+ [ok] live route -> 43.6 min now (free-flow 46.3 min, 58.5 km)
44
+ [ok] live route <- 43.1 min now (free-flow 46.5 min, 58.9 km)
45
+ [ok] estimated API usage: ~938/day, ~6566/week (free tier ~10,000/month)
46
+
47
+ All checks passed - ready for a long run.
48
+ ```
49
+
50
+ Polling with the auto-stop timer:
51
+
52
+ ```text
53
+ > commute poll -d 7d
54
+ Polling '1600 Amphitheatre Parkway, Mountain View, CA' <-> '1 Market St, San Francisco, CA'
55
+ Rush interval 1 min, off-peak 10 min. Ctrl+C to stop.
56
+ Will stop automatically at 2026-07-12 22:59 and generate the report.
57
+ [Sun 22:59:28] -> 43.6 min (free-flow 46.3 min)
58
+ [Sun 22:59:28] <- 43.1 min (free-flow 46.5 min)
59
+ ...
60
+ ```
61
+
62
+ And when the report runs, it also prints the summary:
63
+
64
+ ```text
65
+ 1600 Amphitheatre Parkway, Mountain View, CA -> 1 Market St, San Francisco, CA
66
+ window 07:00-10:00:
67
+ Monday leave 09:45-10:00 (~28.7 min; worst 51.3 min, saves 22.6)
68
+ Tuesday leave 09:45-10:00 (~29.4 min; worst 51.5 min, saves 22.1)
69
+ ...
70
+ ```
71
+
72
+ ## Setup
73
+
74
+ 1. Get a Google Maps Platform API key with the **Routes API** enabled
75
+ (Google Cloud console → APIs & Services → enable "Routes API" → create an
76
+ API key).
77
+
78
+ 2. In the folder where you want the data to live, write the example config
79
+ and edit your addresses:
80
+
81
+ ```
82
+ commute init
83
+ ```
84
+
85
+ 3. Put your API key in a `.env` file in the same folder:
86
+
87
+ ```
88
+ GOOGLE_MAPS_API_KEY=your-key-here
89
+ ```
90
+
91
+ ## Usage
92
+
93
+ Before a long run, verify everything works (config, API key, database, one
94
+ live API call per direction, and a quota estimate for your poll schedule):
95
+
96
+ ```powershell
97
+ commute check
98
+ ```
99
+
100
+ Start polling. With `-d` it stops automatically after the given duration and
101
+ generates the report; without it, it runs until Ctrl+C:
102
+
103
+ ```powershell
104
+ commute poll -d 7d # a full week, then auto-report
105
+ commute poll # open-ended
106
+ ```
107
+
108
+ Durations accept `d`/`h`/`m` and combinations: `7d`, `24h`, `90m`, `1d12h`.
109
+ Add `--no-report` to skip the automatic report at the end.
110
+
111
+ Generate the report any time mid-run (opens `report.html` in your browser;
112
+ `--no-open` to skip that):
113
+
114
+ ```powershell
115
+ commute report
116
+ ```
117
+
118
+ Or watch the run continuously: this regenerates `report.html` *and* a
119
+ day-by-day report (`report-daily.html` — one curve per calendar date plus a
120
+ coverage table, so you can see the data filling in before the week is done).
121
+ Run it in a second terminal alongside `poll`; refresh the browser to see
122
+ updates:
123
+
124
+ ```powershell
125
+ commute watch # updates every 15 minutes
126
+ commute watch -i 5m # or your own interval
127
+ ```
128
+
129
+ To preview the report without waiting a week, seed a synthetic week of
130
+ two-direction data (refuses to run if the DB already has samples). In an
131
+ empty folder:
132
+
133
+ ```powershell
134
+ commute init
135
+ commute demo
136
+ commute report
137
+ ```
138
+
139
+ ### Surviving sleep and reboots
140
+
141
+ Instead of running `poll` in a terminal, register it with the OS so it stays
142
+ alive for the whole run (run this from your data folder — that's where the
143
+ scheduled poller will work):
144
+
145
+ ```powershell
146
+ commute schedule install -d 7d # poll for a week, survive anything
147
+ commute schedule status # is the task/poller alive?
148
+ commute schedule remove # stop and unregister
149
+ ```
150
+
151
+ This relaunches the poller every 5 minutes (a lock file makes that a no-op
152
+ while one is already running) and resumes after a reboot once you log in. The
153
+ stop time is stored as an absolute timestamp, so it survives reboots too;
154
+ when it's reached, the report is generated as usual.
155
+
156
+ Per platform:
157
+
158
+ - **Windows** — a Task Scheduler task; it can also *wake the machine from
159
+ sleep* to poll. Output goes to `poll.log` (no console window).
160
+ - **Linux** — a systemd user service + timer (`commute-poll`). Logs go to
161
+ `journalctl --user -u commute-poll`. Runs from login onward; for
162
+ before-login polling run `loginctl enable-linger $USER`. Linux can't wake
163
+ itself from sleep for this; a missed tick fires right after wake.
164
+ - **macOS** — a launchd LaunchAgent (`com.commute-optimizer.poll`) logging to
165
+ `poll.log`. macOS won't wake for it either — to poll with the lid closed,
166
+ plug in and run `caffeinate -s`.
167
+
168
+ Don't run `schedule install` while a manually-started `poll` from an older
169
+ version is still running — stop that one first, or you'll get duplicate
170
+ samples.
171
+
172
+ If you skip all this and the machine sleeps mid-run, collected data is still
173
+ safe in `commute.db` — the report's Coverage section will show exactly which
174
+ stretches were missed.
175
+
176
+ ## Config
177
+
178
+ ```json
179
+ {
180
+ "origin": "home address",
181
+ "destination": "work address",
182
+ "travel_mode": "DRIVE",
183
+ "track_both_directions": true,
184
+ "poll": {
185
+ "active_window": ["00:00", "23:59"],
186
+ "rush_windows": [["07:00", "10:00"], ["16:00", "19:00"]],
187
+ "rush_interval_min": 1,
188
+ "offpeak_interval_min": 10
189
+ }
190
+ }
191
+ ```
192
+
193
+ - `track_both_directions` — when true (the default), every tick samples both
194
+ origin→destination and the reverse, so one run covers your morning and
195
+ evening commutes.
196
+ - `active_window` — only poll inside this local-time range (set e.g.
197
+ `["06:00", "20:00"]` to skip the night entirely).
198
+ - `rush_windows` — during these windows the poller samples every
199
+ `rush_interval_min` minutes; otherwise every `offpeak_interval_min` minutes.
200
+ The report ranks departure times *within* these windows (otherwise 3 a.m.
201
+ always wins).
202
+
203
+ ## Cost
204
+
205
+ The Routes API (traffic-aware) has ~10,000 free calls/month. The default
206
+ adaptive schedule makes roughly 470 calls/day per direction — ~6,600/week with
207
+ both directions on, still inside the free tier but without room for a second
208
+ full week in the same month. Set `track_both_directions` to false or trim
209
+ `active_window` to halve it. Run `commute check` to see the estimate
210
+ for your exact schedule.
211
+
212
+ Samples land in `commute.db` (SQLite). Transient API failures (network blips,
213
+ rate limits, 5xx) are retried with backoff (5s/15s/45s) before being recorded
214
+ as an error row; permanent failures like a bad key fail fast without retries.
215
+ The report ends each direction with a **Coverage** section flagging any
216
+ stretch where sampling stopped (sleep, reboot, repeated errors) despite the
217
+ schedule expecting data — so you know which medians to trust.
218
+
219
+ ## Known limitations (v1)
220
+
221
+ - **Multi-week data blends together.** The report groups by weekday, so two
222
+ Mondays from different weeks merge into one median curve. Intended use is a
223
+ single week per `commute.db`; delete or rename the DB between runs.
224
+ - **Wake-from-sleep is Windows-only.** On Linux/macOS the scheduled poller
225
+ resumes right after wake but can't wake the machine itself.
226
+ - **No automated tests.** The pieces are small and hand-verified, but there's
227
+ no test suite yet.
File without changes
@@ -0,0 +1,76 @@
1
+ """CLI entry point: python -m commute {poll|report}"""
2
+
3
+ import argparse
4
+
5
+
6
+ def _version():
7
+ try:
8
+ from importlib.metadata import version
9
+ return version("commute-optimizer")
10
+ except Exception: # running from a source checkout without install
11
+ return "dev"
12
+
13
+
14
+ def main():
15
+ p = argparse.ArgumentParser(prog="commute",
16
+ description="Track commute times and find the best departure time.")
17
+ p.add_argument("-c", "--config", help="Path to config.json (default: ./config.json)")
18
+ p.add_argument("--version", action="version", version=f"commute {_version()}")
19
+ sub = p.add_subparsers(dest="cmd", required=True)
20
+ sub.add_parser("init", help="Write an example config.json to the current directory")
21
+ pp = sub.add_parser("poll", help="Start the adaptive polling loop (Ctrl+C to stop)")
22
+ pp.add_argument("-d", "--duration",
23
+ help="Auto-stop after this long and generate the report "
24
+ "(e.g. 7d, 24h, 90m, 1d12h)")
25
+ pp.add_argument("--no-report", action="store_true",
26
+ help="Don't auto-generate the report when the duration ends")
27
+ pp.add_argument("--until",
28
+ help="Auto-stop at this ISO timestamp (what 'schedule' uses "
29
+ "so the deadline survives reboots)")
30
+ rp = sub.add_parser("report", help="Analyze samples and generate report.html")
31
+ rp.add_argument("--no-open", action="store_true", help="Don't open the report in a browser")
32
+ wp = sub.add_parser("watch",
33
+ help="Watch an in-progress run: regenerate the report and a "
34
+ "day-by-day report on an interval")
35
+ wp.add_argument("-i", "--interval", default="15m",
36
+ help="How often to regenerate (default 15m)")
37
+ wp.add_argument("--no-open", action="store_true",
38
+ help="Don't open report-daily.html in a browser")
39
+ sp = sub.add_parser("schedule",
40
+ help="Keep polling alive across sleep and reboots via a "
41
+ "scheduled task (Windows)")
42
+ sp.add_argument("action", choices=["install", "remove", "status"])
43
+ sp.add_argument("-d", "--duration",
44
+ help="With install: stop polling this long from now and "
45
+ "generate the report (e.g. 7d)")
46
+ sub.add_parser("check", help="Preflight: verify config, API key, DB, and live API access")
47
+ sub.add_parser("demo", help="Seed the DB with a synthetic week of two-direction data")
48
+ args = p.parse_args()
49
+
50
+ if args.cmd == "init":
51
+ from .config import init_config
52
+ init_config(args.config)
53
+ elif args.cmd == "poll":
54
+ from . import poller
55
+ poller.run(args.config, duration=args.duration,
56
+ report_on_end=not args.no_report, until=args.until)
57
+ elif args.cmd == "schedule":
58
+ from . import schedule
59
+ schedule.run(args.action, args.config, args.duration)
60
+ elif args.cmd == "watch":
61
+ from . import watch
62
+ watch.run(args.config, interval=args.interval,
63
+ open_browser=not args.no_open)
64
+ elif args.cmd == "check":
65
+ from . import check
66
+ raise SystemExit(check.run(args.config))
67
+ elif args.cmd == "demo":
68
+ from . import demo
69
+ demo.run(args.config)
70
+ else:
71
+ from . import report
72
+ report.run(args.config, open_browser=not args.no_open)
73
+
74
+
75
+ if __name__ == "__main__":
76
+ main()
@@ -0,0 +1,84 @@
1
+ """Preflight check: validate config, API key, database, and make one real
2
+ Routes API call per direction before committing to a week-long run."""
3
+
4
+ from datetime import datetime, timedelta
5
+
6
+ from . import db
7
+ from .config import load_api_key, load_config
8
+ from .poller import current_interval, fetch_route
9
+
10
+
11
+ def _estimate_calls_per_day(cfg):
12
+ """Walk a synthetic day in 1-minute steps and count when a poll would fire."""
13
+ calls = 0
14
+ t = datetime(2026, 1, 5, 0, 0) # any Monday; only time-of-day matters
15
+ next_poll = t
16
+ while t.date() == datetime(2026, 1, 5).date():
17
+ interval = current_interval(cfg, t)
18
+ if interval is not None and t >= next_poll:
19
+ calls += 1
20
+ next_poll = t + timedelta(seconds=interval)
21
+ t += timedelta(minutes=1)
22
+ return calls * (2 if cfg.get("track_both_directions", True) else 1)
23
+
24
+
25
+ def run(config_path=None):
26
+ ok = True
27
+
28
+ # 1. config
29
+ try:
30
+ cfg = load_config(config_path)
31
+ print(f"[ok] config: {cfg['origin']!r} -> {cfg['destination']!r}")
32
+ except SystemExit as e:
33
+ print(f"[FAIL] config: {e}")
34
+ return 1
35
+
36
+ # 2. API key present
37
+ try:
38
+ api_key = load_api_key()
39
+ print(f"[ok] API key found ({api_key[:6]}...{api_key[-4:]})")
40
+ except SystemExit as e:
41
+ print(f"[FAIL] API key: {e}")
42
+ return 1
43
+
44
+ # 3. database writable
45
+ try:
46
+ conn = db.connect()
47
+ conn.execute("SELECT 1")
48
+ n = db.count_samples(conn)
49
+ conn.close()
50
+ print(f"[ok] database writable at {db.DB_PATH} ({n} existing samples)")
51
+ if n:
52
+ print(" note: existing samples will mix into the report — "
53
+ "delete commute.db for a clean run.")
54
+ except Exception as e:
55
+ print(f"[FAIL] database: {e!r}")
56
+ ok = False
57
+
58
+ # 4. live API call per direction
59
+ directions = [("ab", cfg["origin"], cfg["destination"])]
60
+ if cfg.get("track_both_directions", True):
61
+ directions.append(("ba", cfg["destination"], cfg["origin"]))
62
+ for name, origin, destination in directions:
63
+ arrow = "->" if name == "ab" else "<-"
64
+ try:
65
+ r = fetch_route(api_key, origin, destination,
66
+ cfg.get("travel_mode", "DRIVE"))
67
+ print(f"[ok] live route {arrow} {r['duration_s'] / 60:.1f} min now "
68
+ f"(free-flow {r['static_duration_s'] / 60:.1f} min, "
69
+ f"{r['distance_m'] / 1000:.1f} km)")
70
+ except Exception as e:
71
+ msg = getattr(e, "read", None)
72
+ detail = msg().decode(errors="replace")[:300] if msg else repr(e)
73
+ print(f"[FAIL] live route {arrow}: {detail}")
74
+ ok = False
75
+
76
+ # 5. quota estimate
77
+ per_day = _estimate_calls_per_day(cfg)
78
+ per_week = per_day * 7
79
+ print(f"[{'ok' if per_week < 9500 else 'WARN'}] estimated API usage: "
80
+ f"~{per_day}/day, ~{per_week}/week (free tier ~10,000/month)")
81
+
82
+ print("\nAll checks passed — ready for a long run."
83
+ if ok else "\nFix the failures above before starting a long run.")
84
+ return 0 if ok else 1
@@ -0,0 +1,12 @@
1
+ {
2
+ "origin": "1600 Amphitheatre Parkway, Mountain View, CA",
3
+ "destination": "1 Market St, San Francisco, CA",
4
+ "travel_mode": "DRIVE",
5
+ "track_both_directions": true,
6
+ "poll": {
7
+ "active_window": ["00:00", "23:59"],
8
+ "rush_windows": [["07:00", "10:00"], ["16:00", "19:00"]],
9
+ "rush_interval_min": 1,
10
+ "offpeak_interval_min": 10
11
+ }
12
+ }