Repository navigation
feature(Auth): Sign in with aperiodic.login() and find the key on its own - #43
Conversation
… own - aperiodic.login() (or python -m aperiodic login) signs in through aperiodic.io/device, on a laptop, over SSH or in a remote notebook, and saves the machine's own key where the Aperiodic CLI saves its key. - Calls without api_key= now take APERIODIC_API_KEY, then the saved key; preview calls stay on the demo key. The missing-key error says how to sign in. - aperiodic.logout() revokes and removes the key; aperiodic.whoami() shows the key and what the plan covers. - Standard library only, so it works on a bare install. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoDcuvZ8WqH4mnxq31Muur
… hermetic Ruff 0.14.1 (CI's) wants __all__ and the stream module's imports sorted. The test that a call with no key raises now clears APERIODIC_API_KEY and points the saved-key lookup at an empty directory: both are key sources since aperiodic.login(), and CI sets the former. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoDcuvZ8WqH4mnxq31Muur
…he sign-in copy Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoDcuvZ8WqH4mnxq31Muur
Critical review: please address before mergeI checked this against the server routes in dream-faster/unravel-router#1174 ( 1.
|
…tream and get_symbols, clearer errors - login() keeps a saved key the server still accepts and says so; login(force=True) (python -m aperiodic login --force) makes a new key and revokes the one it replaces, as does logging in after the saved key was rejected. - stream(), get_symbols() and get_symbols_async() find the key on their own like every other call. - A 401 for the key saved on this machine names its file and says to run aperiodic.login() again, in REST calls, the stream handshake and whoami. - Polling rides out 5xx, 429 and dropped connections; non-JSON bodies and network errors become AperiodicDataError, so the command line never prints a traceback offline. - logout() returns a LogoutResult saying whether the key was removed and revoked, and the command line prints one accurate line. - The key order is written down in the README: passed key, env, demo key for preview, saved key. The credentials file is written via mkstemp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoDcuvZ8WqH4mnxq31Muur
|
Thanks for the review. Every point is addressed in 18e81f1:
Nits:
ruff 0.14.1 (CI's version) is clean. Generated by Claude Code |
…r-machine key (#30) **Needs the server side first:** [dream-faster/unravel-router#1174](dream-faster/unravel-router#1174) (the `/api/v1/cli/*` routes and the `/device` page) is merged; release this after it's live in production. Until then `aperiodic login` gets a 404 from `/cli/login`, and every other command works as before. ## What changes - **`aperiodic login`:** - If this machine already has a saved key that still works, it says `Already signed in as "<name>" on <plan>. Run 'aperiodic login --force' to replace it.` and stops. Logging in twice doesn't pile up keys. - Otherwise it starts a login and opens `aperiodic.io/device?code=…` in the browser. It only opens an https link on the API's own host; anything else is printed. Over SSH (`SSH_CONNECTION` / `SSH_TTY`), on Linux without a display, or with `--no-browser`, it prints the link to open on any device instead. - It shows the code to check and polls until the person allows it, backing off on `slow_down` and stopping on `access_denied` / `expired_token`. Then it saves the machine's own named key, and when it replaced one, revokes the old one (best effort). - `--name` overrides the key's name, which defaults to `<hostname> (aperiodic CLI)`. - **Credentials file:** `~/.config/aperiodic/credentials`, JSON, mode 0600 in a 0700 directory, written through a temp file and renamed into place. It lives in `$XDG_CONFIG_HOME/aperiodic`, or `%APPDATA%\aperiodic` on Windows; `$APERIODIC_CONFIG_DIR` overrides both. Only a missing `api_key` counts as no credentials. The Python client reads the same file ([aperiodic-io/client#43](aperiodic-io/client#43)). - **Key lookup for every command** (`metric`, `symbols`, `raw`, `stream`): `APERIODIC_API_KEY` first, then `DEMO-KEY` with `--preview`, then the saved key. A saved key never answers a preview request, so a revoked one can't break the free sample. The order is written down in the README, and the Python client uses the same one. - **A rejected saved key** says `The key saved in <path> was rejected (revoked?). Run 'aperiodic login' again.` instead of a bare 401. - **`aperiodic logout`:** revokes this machine's key on the server (`DELETE /cli/key`) and deletes the file. It passes on the server's reason when revoking doesn't happen: for an account's default key it says to rotate it at /home/api, and for a key that's already revoked it says so. If the server can't be reached, it still removes the file and says where to revoke the key by hand. - **`aperiodic whoami`:** shows the plan, which key is in use and where it came from, datasets in full, markets (for a plan held to some, as Free is: "BTC, ETH, SOL and 10 RWA perps"), history and live. - Usage text and README updated. ## Tests - `cli_auth_test.go` runs against an `httptest` stand-in of the server and covers: - login: it opens only safe links, shows the code, polls until the key arrives, and saves it with mode 0600 (checked where the OS has file modes, so not on Windows); - re-login: a working key is kept, `--force` replaces and revokes, and a revoked key is replaced; - over SSH it prints the link and opens nothing; - it backs off on `slow_down`, and a denial saves nothing; - `APERIODIC_API_KEY` wins over the saved key, and `--preview` uses `DEMO-KEY` even with a saved key; - a revoked saved key gets the "log in again" message; - the credentials file tolerates any `created_at`, and saving leaves no temp file behind; - logout revokes and removes the key, and reports the server's reason; - `whoami` output, including the markets line and the demo key; - `login -h` exits 0; - with no key at all, the error points to `aperiodic login`. - `gofmt` and `go vet` (also with `GOOS=windows`) are clean. Every test that needs no live key passes. - The 8 live tests (`requireAPIKey`) fail locally without `APERIODIC_API_KEY`, as they do on `main`. CI sets the key. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MoDcuvZ8WqH4mnxq31Muur --------- Co-authored-by: Claude <noreply@anthropic.com>
Needs the server side first: dream-faster/unravel-router#1174 (the
/api/v1/cli/*routes and/device) is merged; release this after it's live in production. It's the same flow as aperiodic-io/cli#30, and both write the same credentials file. Until the server is deployed,login()gets a 404 when starting, and everything else works as before.What changes
aperiodic.login(), orpython -m aperiodic loginon a machine without the CLI:Already signed in as "<name>" on <plan>. Call aperiodic.login(force=True) to replace it.and returns that login. On the command line the flag is--force. Logging in twice doesn't pile up keys.aperiodic.io/device?code=…link and the code, and polls until the person allows it.slow_downand stops onaccess_denied/expired_token.~/.config/aperiodic/credentials. The file is written through a temp file at mode 0600, with the same path rules as the CLI ($APERIODIC_CONFIG_DIR,$XDG_CONFIG_HOME,%APPDATA%). When it replaced a key, it revokes the old one (best effort).LoginResult(key_name, plan_name, path). The key itself is never printed or returned, so it can't end up in a notebook's output.api_key=first, thenAPERIODIC_API_KEY, then the demo key forpreview=True, then the saved key. A saved key never answers a preview request, so a revoked one can't break the free sample. This applies to every endpoint, includingstream(),get_symbols()andget_symbols_async(), which now takeapi_key=None. The README spells out the order, and the CLI uses the same one.The key saved in <path> was rejected (revoked?). Run aperiodic.login() again.instead of a bare 401.aperiodic.logout()revokes the key (DELETE /cli/key), removes the file and returnsLogoutResult(removed, revoked, message). The message says what happened: revoked, already revoked, or the account's default key (rotate it at /home/api).aperiodic.whoami()returns the key and what its plan covers.python -m aperiodic whoamiprints it, with a Markets line for a plan held to some (Free: "BTC, ETH, SOL and 10 RWA perps").aperiodic.login()or setAPERIODIC_API_KEY.Error: Could not reach …instead of a traceback.urllib): the core install has no HTTP client (httpx is an extra). In Pyodide,login()explains it needs regular Python.Tests
tests/test_auth.pyruns against a localThreadingHTTPServerstand-in of the server and covers:force=Truereplaces and revokes, and a revoked key is replaced;slow_downand survives 5xx / 429 / non-JSON pages;login/whoamifail with a message, not a traceback;get_symbols(sync and async) finds the saved key;whoamiand thepython -m aperiodicentry point, including the Markets line and--force;tests/test_stream.py:stream()uses the saved key when none is passed, and a rejected saved key says to log in again.test_auth,test_readmeandtest_streampass (89). ruff 0.14.1 (CI's version) is clean.main(they need a real key and network).🤖 Generated with Claude Code
https://claude.ai/code/session_01MoDcuvZ8WqH4mnxq31Muur