Done. Here's what I did for issue #95.
Problem
An invalid/expired provider API token caused an uncaught digitalocean.DataReadError to bubble up as a raw Python traceback. The DigitalOcean provider only wrapped a few calls (create_vm, destroy_vm), so read paths like list_vms → get_all_droplets crashed ungracefully.
Fix
Added a top-level entry-point wrapper cli() in machine/main.py that runs the Click group and catches the provider SDKs' base API exceptions, converting them into a clear message instead of a traceback:
- Catches
digitalocean.Error, vultr.VultrException, and (lazily) the Google Cloud auth/API exception bases — so it's a safety net across all subcommands and providers, not just list.
- Auth-style failures (401/403/"unable to authenticate"/permission) get an actionable message pointing at the config token; other API errors report the underlying detail.
--debug re-raises the original exception so the full traceback is still available for troubleshooting.
- Non-provider exceptions (real bugs) are not swallowed — they still surface.
Updated [project.scripts] in pyproject.toml to point machine at machine.main:cli (the shiv build uses this console script automatically).
Now:
$ machine --config-file … list
Error: the cloud provider rejected the request as unauthenticated or unauthorized.
Check that the API token/key in your config file is correct and has not expired.
Verification
- Reproduced the original traceback, then confirmed the graceful message (exit 1) after the fix, and that
--debug still shows the traceback.
- Confirmed normal commands (
version, types), Click usage errors, and help are unaffected.
- Added hermetic tests in
tests/test_error_handling.py (message formatting, wrapper catches provider errors, --debug re-raises, non-provider exceptions pass through).
- Full suite: 52 passed, lint clean.
I haven't committed — let me know if you'd like me to.