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
Zero-config: just import and run
Inspect what auto-discovery picked up
Explicit registration (one credential)
Multiple credentials with
eeCreds.use()Scoped switching with the
withcontext managerAll the input formats
addCredsacceptsOAuth personal-account refresh tokens
Inspecting state
Map.view()integrationAuth mode env var (
GEEVIZ_EEAUTH_MODE)Stopping and restarting
Embedding the proxy in your own FastAPI app
Standalone CLI
Prerequisites
pip install geeviz(pullsfastapi,uvicorn,httplib2,earthengine-api)One of:
Persistent EE auth (run
earthengine authenticateonce) — covers most cellsOr gcloud Application Default Credentials with a quota project set
Optional: set
GEEVIZ_DEMO_SA_PATH=/path/to/your/sa.jsonbefore 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 |
|
Start proxy + init EE |
|
Switch active cred |
|
Switch for a block |
|
What’s registered? |
|
Currently active? |
|
Shut down proxy |
|
Auto-discover + start |
|
Run the full bootstrap |
|
Get a FastAPI router |
|
Standalone server |
|
Lock to proxy in prod |
|
Skip proxy entirely |
|
Auto-discovery sources (in priority order):
$GOOGLE_APPLICATION_CREDENTIALS→ registered as"adc"~/.config/earthengine/credentials→ registered as"ee-persistent"$GEE_SERVICE_ACCOUNT_B64→ registered as"env-default"$GEE_<NAME>_SERVICE_ACCOUNT→ registered as<name>(lowercased)