Blog
Guides

How to test OAuth connections locally

DK
Dan Kowalsky
Founder ·

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/callback added 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 connect

You 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 quit

Press 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_count
Using your local Google connection ([email protected])
42 unread

That'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:

  1. It starts a small temporary web server on localhost:8765 and opens the service's sign-in page.
  2. After you allow access, the service sends your browser back to localhost, where the CLI picks up a one-time code.
  3. 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

CommandWhat it does
call connectMenu of connections and their state
call connect googleGo straight to one
call disconnectForget tokens on this machine
call connect --port 9000Use another port (add that redirect URI to your app)
call connect --no-browserPrint 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.

See how it works

Keep reading