geeViz.eeAuth — multi-credential Earth Engine in one Python process

This notebook walks through every way to use geeViz.eeAuth — the auth-proxy library that lets one Python process talk to Earth Engine on behalf of many service accounts and/or personal-account refresh tokens, without the usual ee.Initialize() global-state collision.

Designed to run end-to-end on a typical setup (one EE persistent token from earthengine authenticate). The cells that need extra service-account JSON keys check for a path you can set via GEEVIZ_DEMO_SA_PATH and skip cleanly if it’s not configured.

Sections

  1. Zero-config: just import and run

  2. Inspect what auto-discovery picked up

  3. Explicit registration (one credential)

  4. Multiple credentials with eeCreds.use()

  5. Scoped switching with the with context manager

  6. All the input formats addCreds accepts

  7. OAuth personal-account refresh tokens

  8. Inspecting state

  9. Map.view() integration

  10. Auth mode env var (GEEVIZ_EEAUTH_MODE)

  11. Stopping and restarting

  12. Embedding the proxy in your own FastAPI app

  13. Standalone CLI

Prerequisites

  • pip install geeviz (pulls fastapi, uvicorn, httplib2, earthengine-api)

  • One of:

    • Persistent EE auth (run earthengine authenticate once) — covers most cells

    • Or gcloud Application Default Credentials with a quota project set

  • Optional: set GEEVIZ_DEMO_SA_PATH=/path/to/your/sa.json before launching the kernel to exercise the SA-specific cells. They skip cleanly if unset.

Setup — a single helper used throughout

All later cells reference DEMO_SA_PATH and skip themselves if it’s not pointing at a real file. Set it once via env var or edit the literal here.

import os
DEMO_SA_PATH = r"C:\tmp\your-service-account-json.json"
# Edit this OR set GEEVIZ_DEMO_SA_PATH in your shell before launching the kernel.
# DEMO_SA_PATH = os.path.expanduser(os.environ.get("GEEVIZ_DEMO_SA_PATH", ""))

HAS_SA = bool(DEMO_SA_PATH) and os.path.isfile(DEMO_SA_PATH)
if HAS_SA:
    print(f"DEMO_SA_PATH = {DEMO_SA_PATH!r} — SA-using cells will run")
else:
    print(
        "DEMO_SA_PATH not configured — SA-using cells will skip.\n"
        "To exercise them, set GEEVIZ_DEMO_SA_PATH to an SA JSON key path "
        "and re-run the kernel, or edit DEMO_SA_PATH above."
    )
DEMO_SA_PATH = 'C:\\tmp\\your-service-account-json.json' — SA-using cells will run

1. Zero-config — just import and run

If you’ve ever run earthengine authenticate, geeViz finds that credential and uses it through the proxy automatically. No changes to your existing scripts.

Under the hood, importing geeViz.geeView triggers robustInitializer(), which delegates to geeViz.eeAuth.robust_init. That tries (in order): already-initialized fast-path → eeAuth proxy via ensure_started → plain ee.Initialize() (letting EE auto-resolve the project from credentials / ADC) → interactive ee.Authenticate(force=True, auth_mode='localhost').

import ee
from geeViz.geeView import Map

print("Map.project =", Map.project)
print("1 + 1 (via EE) =", ee.Number(1).add(1).getInfo())
Map.project = askterra-geospatial-agent
1 + 1 (via EE) = 2

2. Inspect what auto-discovery picked up

After import, the singleton eeCreds carries whatever credentials robust_init discovered.

from geeViz.eeAuth import eeCreds

print("Registered creds:", eeCreds.list())
print("Active cred:    ", eeCreds.current() or "<none>")
print("Proxy URL:      ", eeCreds.proxy_url or "<not running>")

if not eeCreds.list():
    print(
        "\nNote: empty list means EE was initialized via plain "
        "`ee.Initialize()` (using gcloud ADC or the EE persistent "
        "credentials\' stored project) rather than the multi-tenant "
        "proxy. Single-tenant workflows do not need the proxy - "
        "it is only useful when you want to switch between multiple "
        "credentials in one process."
    )
Registered creds: ['ee-persistent', 'adc-default']
Active cred:     ee-persistent
Proxy URL:       http://127.0.0.1:8889/ee-api

3. Explicit registration (one credential)

Skip auto-discovery and register a specific service-account JSON yourself. This cell is skipped if DEMO_SA_PATH isn’t configured.

if not HAS_SA:
    print("Skipped (DEMO_SA_PATH not set).")
else:
    from geeViz.eeAuth import eeCreds
    import ee

    # Reset the singleton so we register from a clean slate.
    eeCreds.stop()
    eeCreds._entries.clear()

    eeCreds.addCreds(DEMO_SA_PATH, name="prod")
    eeCreds.start()

    # With only one credential, no `.use()` needed — it's the default.
    print(eeCreds.current(), "→", ee.Number(2).getInfo())
[geeViz.eeAuth] EE initialized via proxy: http://127.0.0.1:8890/ee-api (tenant_header=X-geeViz-Creds)
prod → 2

4. Multiple credentials with .use(name)

Register N credentials, switch between them at runtime. For this demo we register the same SA under two different names (since most users won’t have two real SAs handy). Replace the second addCreds with a different file when you do.

if not HAS_SA:
    print("Skipped (DEMO_SA_PATH not set).")
else:
    from geeViz.eeAuth import eeCreds
    import ee

    eeCreds.stop()
    eeCreds._entries.clear()

    (
        eeCreds
        .addCreds(DEMO_SA_PATH, name="prod")
        .addCreds(DEMO_SA_PATH, name="training")  # swap for a second SA in real use
    )
    eeCreds.start()

    eeCreds.use("prod")
    print("As prod:    ", ee.Number(1).getInfo())

    eeCreds.use("training")
    print("As training:", ee.Number(1).getInfo())
[geeViz.eeAuth] EE initialized via proxy: http://127.0.0.1:8890/ee-api (tenant_header=X-geeViz-Creds)
As prod:     1
As training: 1

5. Scoped switching with with eeCreds.use(...)

When you want a credential for one block of code and the previous one automatically restored after.

if not HAS_SA:
    print("Skipped (DEMO_SA_PATH not set).")
else:
    from geeViz.eeAuth import eeCreds

    eeCreds.use("prod")
    print("Before with-block:", eeCreds.current())

    with eeCreds.use("training"):
        print("Inside with-block:", eeCreds.current())  # 'training'

    print("After with-block: ", eeCreds.current())       # 'prod'
Before with-block: prod
Inside with-block: training
After with-block:  prod
if not HAS_SA:
    print("Skipped (DEMO_SA_PATH not set).")
else:
    from geeViz.eeAuth import eeCreds

    # Nesting also works — each layer restores the previous on exit.
    with eeCreds.use("prod"):
        print("Outer:", eeCreds.current())
        with eeCreds.use("training"):
            print("  Inner:", eeCreds.current())
        print("Outer again:", eeCreds.current())
Outer: prod
  Inner: training
Outer again: prod
# Concurrent / async-safe: each task carries its own credential context
# (CURRENT_TENANT is a ContextVar, so .use() inside one async task
# never leaks into another).
if not HAS_SA:
    print("Skipped (DEMO_SA_PATH not set).")
else:
    import asyncio
    from geeViz.eeAuth import eeCreds

    async def task(name):
        with eeCreds.use(name):
            print(f"async task {name}: current={eeCreds.current()}")

    await asyncio.gather(task("prod"), task("training"))
async task prod: current=prod
async task training: current=training

6. All the input formats addCreds accepts

Auto-detected — pass whatever’s easiest for the situation.

if not HAS_SA:
    print("Skipped (DEMO_SA_PATH not set).")
else:
    from geeViz.eeAuth import eeCreds
    import base64, json

    eeCreds.stop()
    eeCreds._entries.clear()

    # 6a. File path to an SA JSON key
    eeCreds.addCreds(DEMO_SA_PATH, name="from-path")

    # 6b. File path with ~ (expanded)
    # eeCreds.addCreds("~/.config/earthengine/credentials", name="from-home")

    # 6c. Base64-encoded SA JSON (typical for env vars)
    with open(DEMO_SA_PATH, "rb") as f:
        sa_b64 = base64.b64encode(f.read()).decode("ascii")
    eeCreds.addCreds(sa_b64, name="from-base64")

    # 6d. JSON string literal
    sa_json_str = open(DEMO_SA_PATH).read()
    eeCreds.addCreds(sa_json_str, name="from-json-string")

    # 6e. Already-parsed dict
    sa_dict = json.loads(sa_json_str)
    eeCreds.addCreds(sa_dict, name="from-dict")

    # 6f. Raw bytes (utf-8 or base64)
    eeCreds.addCreds(open(DEMO_SA_PATH, "rb").read(), name="from-bytes")

    print("All registered:", eeCreds.list())
All registered: ['from-path', 'from-base64', 'from-json-string', 'from-dict', 'from-bytes']

7. OAuth personal-account refresh tokens

Service accounts aren’t the only option. Personal credentials work too — the same credentials earthengine authenticate produces.

from geeViz.eeAuth import eeCreds
import ee.oauth

ee_creds_path = ee.oauth.get_credentials_path()
if not os.path.isfile(ee_creds_path):
    print(f"Skipped — no EE credentials file at {ee_creds_path}. "
          "Run `earthengine authenticate` first.")
else:
    # The standard EE persistent file location. Auto-detected as OAuth.
    eeCreds.stop()
    eeCreds._entries.clear()
    eeCreds.addCreds(ee_creds_path, name="me")
    
    for nm in eeCreds.list():
        info = eeCreds.info(nm)
        print(f"  {nm:12} type={info['type']:5} project={info['project_id']!r}")
  me           type=oauth project=''
# You can also build an OAuth credential dict by hand — e.g. from a
# refresh-token API. The keys mirror `google.oauth2.credentials.Credentials`.
# (Not executed here — it requires real client_id/secret/refresh_token.)
demo_dict = {
    "type": "authorized_user",
    "client_id": "1234.apps.googleusercontent.com",
    "client_secret": "GOCSPX-...",
    "refresh_token": "1//0g...",
}
print(
    "To register a manually-constructed OAuth dict:\n"
    "  eeCreds.addCreds(demo_dict, name='colleague', "
    "project='YOUR_PROJECT')\n"
    "(OAuth credentials don't carry a project — pass `project=` to\n"
    " tell the proxy which one to bill.)"
)
To register a manually-constructed OAuth dict:
  eeCreds.addCreds(demo_dict, name='colleague', project='YOUR_PROJECT')
(OAuth credentials don't carry a project — pass `project=` to
 tell the proxy which one to bill.)

8. Inspecting state

Quick reference for figuring out what’s loaded.

from geeViz.eeAuth import eeCreds

print("list()     →", eeCreds.list())
print("current()  →", eeCreds.current())
print("proxy_url  →", eeCreds.proxy_url)
if eeCreds.list():
    print("info()     →", eeCreds.info())
    print("has(name)  →", eeCreds.has(eeCreds.current()))
list()     → ['me']
current()  → me
proxy_url  → None
info()     → {'name': 'me', 'type': 'oauth', 'source': 'path:C:\\Users\\ianho/.config/earthengine/credentials', 'project_id': '', 'client_email': ''}
has(name)  → True

9. Map.view() integration

Map.view() automatically routes its EE traffic through the same proxy. No URL-baked tokens, no ~1h expiry. Switching credentials in Python carries over to the interactive viewer via the ?tenant= URL param when multiple credentials are registered.

from geeViz.geeView import Map
import ee

Map.clearMap()
Map.addLayer(
    ee.Image("USGS/SRTMGL1_003"),
    {"min": 0, "max": 4000, "palette": "green,yellow,white"},
    "SRTM",
)
Map.view()
Adding layer: SRTM
Starting webmap
Using eeCreds proxy at http://127.0.0.1:8889/ee-api (creds=me)
geeView URL: http://127.0.0.1:8889/geeView/

10. Mode override — GEEVIZ_EEAUTH_MODE

The env var controls how robustInitializer and Map.view() pick between the proxy and the legacy direct-token path. Set BEFORE import geeViz.geeView for it to affect the import-time initializer.

# --- auto (default) ---
# Tries the proxy first. Falls back to legacy direct-token on failure.
# Best for development.
# os.environ["GEEVIZ_EEAUTH_MODE"] = "auto"

# --- proxy ---
# Demands the proxy. Raises RuntimeError if no credentials can be
# discovered or the proxy can't start. Use in production to lock in
# the modern path and fail loudly if misconfigured.
# os.environ["GEEVIZ_EEAUTH_MODE"] = "proxy"

# --- legacy ---
# Skips the proxy entirely. Use only if you have a specific reason
# (e.g. constrained env where local sockets aren't allowed).
# Note: tokens will appear in viewer URLs and expire after ~1h.
# os.environ["GEEVIZ_EEAUTH_MODE"] = "legacy"

print("Current mode:", os.environ.get("GEEVIZ_EEAUTH_MODE", "auto (default)"))
Current mode: auto (default)

11. Stopping and restarting

from geeViz.eeAuth import eeCreds

# Shut down the proxy thread. Safe to call when not started.
eeCreds.stop()
print("After stop: proxy_url =", eeCreds.proxy_url)

# Registered creds are still there; restart the proxy any time:
if eeCreds.list():
    status = eeCreds.start()
    print("After restart:", status)
After stop: proxy_url = None
After restart: {'started': True, 'proxy_url': 'http://127.0.0.1:8890/ee-api', 'tenants': ['me'], 'current': 'me'}
[geeViz.eeAuth] EE initialized via proxy: http://127.0.0.1:8890/ee-api (tenant_header=X-geeViz-Creds)

12. Embedding the proxy in your own FastAPI app

If you already run a FastAPI service (an MCP server, a custom dashboard, an internal API), mount the eeAuth proxy as one of your routes. These cells construct apps but don’t run them — they’re templates.

# --- 12a. Default: env-var SARegistry ---
# Loads SAs from GEE_SERVICE_ACCOUNT_B64 + GEE_<NAME>_SERVICE_ACCOUNT
# Use when your tenants come from deployment env vars.
from fastapi import FastAPI
from geeViz.eeAuth.server import build_proxy_router

app_12a = FastAPI()
app_12a.include_router(build_proxy_router(), prefix="/ee-api")
print("app_12a routes:", [r.path for r in app_12a.routes if hasattr(r, 'path')])
app_12a routes: ['/openapi.json', '/docs', '/docs/oauth2-redirect', '/redoc', '/ee-api/health', '/ee-api/{path:path}']
# --- 12b. With an explicit eeCreds instance ---
# Use when you want programmatic control over which credentials are loaded.
if not HAS_SA:
    print("Skipped (DEMO_SA_PATH not set).")
else:
    from fastapi import FastAPI
    from geeViz.eeAuth.eeCreds import EECreds

    creds = EECreds()  # private instance, separate from the global singleton
    creds.addCreds(DEMO_SA_PATH, "prod")
    creds.addCreds(DEMO_SA_PATH, "training")  # swap for a 2nd SA in real use

    app_12b = FastAPI()
    app_12b.include_router(creds.router(), prefix="/ee-api")
    print("app_12b routes:", [r.path for r in app_12b.routes if hasattr(r, 'path')])
app_12b routes: ['/openapi.json', '/docs', '/docs/oauth2-redirect', '/redoc', '/ee-api/health', '/ee-api/{path:path}']
# --- 12c. With custom callbacks (advanced) ---
# Override the tenant resolver and workload-tag builder to integrate
# with your own auth scheme (e.g. IAP, JWTs, custom headers).
from fastapi import FastAPI
from geeViz.eeAuth.server import build_proxy_router
from geeViz.eeAuth.tags import build_workload_tag

USER_TO_TENANT = {"[email protected]": "prod", "[email protected]": "training"}

def my_tenant_resolver(request):
    # Example: resolve from an IAP/JWT header, fall back to ?tenant param
    user = request.headers.get("X-MyOrg-User", "").lower()
    return USER_TO_TENANT.get(user, request.query_params.get("tenant", ""))

def my_workload_tag_builder(request, tenant):
    user = request.headers.get("X-MyOrg-User", "anonymous")
    return build_workload_tag("my-svc", tenant, user)

app_12c = FastAPI()
app_12c.include_router(
    build_proxy_router(
        tenant_header="X-MyOrg-Tenant",
        tenant_resolver=my_tenant_resolver,
        workload_tag_builder=my_workload_tag_builder,
    ),
    prefix="/ee-api",
)
print("app_12c routes:", [r.path for r in app_12c.routes if hasattr(r, 'path')])
app_12c routes: ['/openapi.json', '/docs', '/docs/oauth2-redirect', '/redoc', '/ee-api/health', '/ee-api/{path:path}']

13. Standalone CLI

For deploying the proxy as its own service (Cloud Run, Docker, etc.) without any Python integration.

# Configure tenants via env vars then run
export GEE_SERVICE_ACCOUNT_B64=$(base64 -w0 sa-default.json)
export GEE_TRAINING_SERVICE_ACCOUNT=$(base64 -w0 sa-training.json)

python -m geeViz.eeAuth --port 8888

Then point any EE client at http://localhost:8888/ee-api:

# Health check + tenant listing
curl http://localhost:8888/

# A real EE call as the default tenant
curl http://localhost:8888/ee-api/v1/projects/earthengine-legacy/algorithms

# Same call as the training tenant
curl -H 'X-geeViz-Creds: training' \
  http://localhost:8888/ee-api/v1/projects/earthengine-legacy/algorithms

From Python on another machine:

from geeViz.eeAuth import initialize_via_proxy, tenant_context
import ee

initialize_via_proxy("http://my-proxy-host:8888/ee-api")

with tenant_context("training"):
    ee.Image(1).getInfo()    # routes via the remote proxy as training

Quick reference

What you want

API

Register a credential

eeCreds.addCreds(input, name=...)

Start proxy + init EE

eeCreds.start()

Switch active cred

eeCreds.use(name)

Switch for a block

with eeCreds.use(name): ...

What’s registered?

eeCreds.list() / eeCreds.info(name)

Currently active?

eeCreds.current()

Shut down proxy

eeCreds.stop()

Auto-discover + start

eeCreds.ensure_started(mode='auto')

Run the full bootstrap

eeCreds.robust_init()

Get a FastAPI router

eeCreds.router(...)

Standalone server

python -m geeViz.eeAuth --port 8888

Lock to proxy in prod

GEEVIZ_EEAUTH_MODE=proxy

Skip proxy entirely

GEEVIZ_EEAUTH_MODE=legacy

Auto-discovery sources (in priority order):

  1. $GOOGLE_APPLICATION_CREDENTIALS → registered as "adc"

  2. ~/.config/earthengine/credentials → registered as "ee-persistent"

  3. $GEE_SERVICE_ACCOUNT_B64 → registered as "env-default"

  4. $GEE_<NAME>_SERVICE_ACCOUNT → registered as <name> (lowercased)