How to test OAuth connections locally
OAuth is usually where testing a script locally gets painful. You copy a token out of a browser, paste it into a file, and it expires an hour later. Or you write a throwaway sign-in script that grows its own redirect handling, token cache and refresh logic, all to try a ten-line function.
With Callables you run one command instead: call connect. It signs you in with your own OAuth app, keeps the tokens on your machine, and call local hands them to your function the same way the platform does in production.
What you need
- The Callables CLI, version 0.5.0 or later:
pip install -U callables - An OAuth app with the service. See the setup guide for your service, such as Google or Microsoft.
http://localhost:8765/callbackadded to the app's redirect URIs- A function that declares a connection:
from callables import Connection, deploy
google = Connection("google", provider="google", scopes=["gmail.readonly"])
@deploy(title="Unread count", connections=[google])
def unread_count() -> str:
"""How many unread emails are in your inbox."""
import requests
resp = requests.get("https://gmail.googleapis.com/gmail/v1/users/me/labels/INBOX",
headers={"Authorization": f"Bearer {google.token}"})
resp.raise_for_status()
return f"{resp.json()['messagesUnread']} unread"1. Put the app's keys in .env
GOOGLE_CLIENT_ID=1234567890-abc.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-...These are the same values you set as project secrets on Callables. call loads .env from your project folder, the folder you run from, or the top of the git repo, so one .env can serve a repo with several projects.
2. Connect
call connectYou get a menu of the connections your project uses and whether each one is ready:
Connections in this project (✓ connected ! reconnect · not connected ✗ needs .env)
❯ 1. · google Google: not connected
↑/↓ or a number, Enter to connect or reconnect, q to quitPress Enter. Your browser opens Google's sign-in, showing your app's name and the permissions it asks for. Allow it (on Google's screen, tick every box), and the terminal confirms:
Connected Google as [email protected]. `call local` uses it from now on.You're back at the menu, so you can connect the next service or press q.
3. Run your function
call local unread_countUsing your local Google connection ([email protected])
42 unreadThat's it. google.token, google.account and the rest work exactly as they do on the platform, and your code doesn't change between the two.
What happens behind the scenes
call connect uses the standard sign-in for command-line tools, the same one gcloud and gh use:
- It starts a small temporary web server on
localhost:8765and opens the service's sign-in page. - After you allow access, the service sends your browser back to
localhost, where the CLI picks up a one-time code. - The CLI exchanges the code for tokens with your app's client ID and secret.
Callables isn't involved. The sign-in goes straight from your machine to the service. A random state value and PKCE make sure the CLI only accepts the code it asked for. If you don't grant every permission the code asks for, nothing is saved and it tells you what's missing.
Tokens are kept in ~/.config/callables/connections.json (%APPDATA% on Windows), readable only by you and never in your repo. Access tokens expire, usually after an hour, and call local refreshes them as needed, so you sign in once.
When your code changes
The menu tracks your code. Add a scope:
google = Connection("google", provider="google", scopes=["gmail.readonly", "calendar.readonly"])and the next call connect marks Google with ! and says the code now asks for more permissions. call local tells you the same thing instead of failing halfway through a run. Reconnect to grant it.
Handy commands
| Command | What it does |
|---|---|
call connect | Menu of connections and their state |
call connect google | Go straight to one |
call disconnect | Forget tokens on this machine |
call connect --port 9000 | Use another port (add that redirect URI to your app) |
call connect --no-browser | Print the sign-in link instead of opening it |
CI and other tokens. If GOOGLE_ACCESS_TOKEN is already set in your shell or .env, call local uses it and skips the stored connection. That's useful in CI, or with a token you got some other way.
Then push
When it works locally, push. On Callables, each person who runs the function connects their own account from the web app, and every run uses theirs. More on that in sharing a script that runs as each user.
Turn your scripts into tools your team can run
Add @deploy to a Python function and push. Your team gets a form, schedules and run history.
Keep reading
How to share a Python script that runs as each user
Some scripts should use a shared company key. Others should act as the person running them, on their own inbox or calendar. How to build and share the second kind without handling anyone's password or token.
GuidesTest your automations locally before you push
Check for deploy problems and run functions on your own machine, with the same input validation as the platform, before anything goes live.
GuidesDeploy from a monorepo or subfolder, with system packages
Build just one folder of a bigger repo, choose the Python version, install dependencies and add tools like ffmpeg with apt.txt.