geeViz.eeAuth.eeCreds¶
- geeViz.eeAuth.eeCreds¶
Multi-tenant credential registry + EE init lifecycle.
Usually used via the module-level singleton
eeCreds— but instantiable directly when you need multiple independent registries (e.g. tests, multi-tenant servers with isolated credential sets):from geeViz.eeAuth.eeCreds import EECreds creds = EECreds() creds.addCreds(...) creds.start()
Underlying module
eeCreds — friendly API for using multiple Earth Engine credentials
interchangeably from the same Python process.
You don’t need to call this directly if you only use one credential.
Importing geeViz.geeView and calling Map.view() already
auto-starts the proxy under the hood via ensure_started("attached") —
single-credential workflows get the proxy for free. Multi-credential
workflows use this module to register, switch, and stop credentials
explicitly.
Supports both service accounts and OAuth user-account refresh tokens, in any of these input formats:
File path to a JSON key
Base64-encoded JSON string
JSON string
Dict (already-parsed JSON)
Raw bytes (UTF-8 JSON or base64 of either)
Auto-start behaviour (the default path)¶
from geeViz.geeView import Map
import ee
# That's it. Map.view() called below will:
# 1. Discover credentials in the environment
# 2. Start a local proxy (uvicorn in a daemon thread) if needed
# 3. Point ee.Initialize at the proxy
Map.addLayer(ee.Image("USGS/SRTMGL1_003"), {"min": 0, "max": 4000}, "SRTM")
Map.view()
The proxy survives for the lifetime of the Python process — subsequent
Map.view() calls reuse it without restarting.
Explicit multi-credential workflow¶
from geeViz.eeAuth import eeCreds
import ee
eeCreds.addCreds("path/to/sa-prod.json", name="prod")
eeCreds.addCreds(b64_sa_string, name="acme")
eeCreds.addCreds("~/.config/earthengine/credentials", name="ian") # OAuth
eeCreds.start() # initializes ee + spawns local proxy
eeCreds.use("acme")
ee.Image(1).getInfo() # routes through the acme SA
# Or scoped switching — restores previous tenant on block exit
with eeCreds.use("ian"):
ee.Image(2).getInfo() # routes through ian's OAuth refresh token
Why this exists¶
ee.Initialize() stores credentials in module-global state — you can
only have one active identity per Python process. eeCreds works
around that by running a local proxy server that holds N credentials,
re-signs each EE REST request with the right one, and lets the SDK’s
single ee.Initialize point at the proxy. The proxy reads which
credential to use from a thread-aware ContextVar set by
eeCreds.use().
Same machinery powers the JS / browser path: each browser tab that
Map.view() opens has its tenant baked into the per-session run_js
file (NOT the page URL), so every tab routes through the right
credential for its lifetime — process-wide eeCreds.use() switches
can’t drift an open tab to a different credential.
- class geeViz.eeAuth.eeCreds.EECreds[source]¶
Bases:
objectMulti-tenant credential registry + EE init lifecycle.
Usually used via the module-level singleton
eeCreds— but instantiable directly when you need multiple independent registries (e.g. tests, multi-tenant servers with isolated credential sets):from geeViz.eeAuth.eeCreds import EECreds creds = EECreds() creds.addCreds(...) creds.start()
- setTagStore(store) EECreds[source]¶
Replace the default
TagStoreused for auto-minted workload tags. Pass anInMemoryTagStore(),SQLiteTagStore(path=...), or any object matching theTagStoreprotocol. Call this BEFOREstart()if you want the proxy’s fallback builder to write into your store. Returns self for chaining.
- getTagStore()[source]¶
Return the current tag store, constructing the default lazily on first call. See
setTagStore()for how to override.
- setTagSecret(secret: str) EECreds[source]¶
Set the secret used when minting workload tags. Defaults to
$WORKLOAD_TAG_SECRET(or a local-dev fallback if unset). Same secret is required to re-mint the same tag from the same parts across processes.
- setWorkloadTag(**parts) str[source]¶
Mint a tag from
parts, store the mapping, and set it on the EE Python SDK so every subsequent EE call carries it.Three things happen atomically:
tag = mint_workload_tag(parts, secret=…)— deterministic short hash (wl_<hex>).getTagStore().put(tag, parts)— solookupWorkloadTag(tag)can recoverpartslater, and any Cloud Monitoring row tagged withtagcan be joined back to identity.ee.data.setWorkloadTag(tag)— SDK-side setter. The tag is attached to every subsequent.getInfo(),.getMapId(), export, etc., AND baked into any tile URL returned bygetMapId, so browser tile fetches inherit it.
Since the proxy’s default builder honours client-set tags (
if request.query_params['workloadTag']: return it), the same tag round-trips through the proxy without any custom builder needed.Returns the minted tag so callers can log / correlate it.
- lookupWorkloadTag(tag: str) dict | None[source]¶
Recover the parts dict for a previously-minted tag. Returns
Noneif the tag isn’t in this process’s tag store (could be from another instance if not using shared storage, or an untagged / externally-tagged request).
- clearWorkloadTag() None[source]¶
Clear the SDK-side workload tag so subsequent EE calls fall back to the proxy’s default builder. Mapping in the store is preserved (so historical tags remain reversible).
- addCreds(creds: str | dict | bytes, name: str, project: str | None = None) EECreds[source]¶
Register a set of credentials under
name.Accepts:
File path to a JSON key file (SA, OAuth, WIF, or impersonation config)
Base64-encoded JSON string (typical for env vars)
JSON string (literal contents)
Already-parsed dict
Bare service-account email string (
foo@bar.iam.gserviceaccount.com) → routed toaddImpersonation()so the proxy mints tokens via the IAM credentials API at request time (no key held)
Auto-detects which credential shape it is.
projectoverrides the default project — required for OAuth + impersonation, optional for SAs since their JSON hasproject_id.Returns self so chaining works:
eeCreds.addCreds(sa1, "a").addCreds(sa2, "b").start()
- addImpersonation(target_email: str, name: str, project: str | None = None) EECreds[source]¶
Register a tenant that mints tokens by impersonating
target_emailat request time.No key material is held. The runtime’s ADC source must hold
roles/iam.serviceAccountTokenCreatoron the target SA. On each token mint,google.auth.impersonated_credentialscalls the IAM credentials API to fetch a short-lived (1h) access token; google-auth refreshes it automatically when it expires.This is the multi-tenant keyless path: the proxy holds N SA emails, not N JSON keys. Works identically across GCP Cloud Run (source = attached SA), AWS via WIF (source = federated identity from STS), and local dev (source = gcloud user creds).
- Parameters:
target_email – The SA to impersonate (
foo@proj.iam.gserviceaccount.com).name – Tenant name for
eeCreds.use(name).project – Quota project for EE calls. If omitted, falls back to the runtime’s default ADC project.
- addADC(name: str = 'adc', project: str | None = None) EECreds[source]¶
Register the runtime’s Application Default Credentials as a tenant entry.
Use this when you want the proxy to route through ADC (attached SA on Cloud Run, WIF federation on AWS, gcloud user creds locally) — without holding any key material. Without this, ADC-only deployments still get EE working via
robust_init()but the proxy starts empty and the multi-tenant features are inaccessible.Tokens are minted by calling
google.auth.default()at mint time, then refreshing. Same code path as keyed SAs from the proxy’s perspective.
- discover(*, overwrite: bool = False) list[str][source]¶
Scan the environment for credentials and register any found.
Lookups, in order — each one that produces a credential gets added under a stable name:
Source
Registered as
$GOOGLE_APPLICATION_CREDENTIALS(JSON path)"adc"~/.config/earthengine/credentials(EE persistent)"ee-persistent"gcloud ADC well-known file
"adc-default"$GEE_SERVICE_ACCOUNT_B64(legacy default SA)"env-default"$GEE_<NAME>_SERVICE_ACCOUNT(per-tenant SA keys)<name>$GEE_<NAME>_SA_EMAIL(per-tenant impersonation)<name>google.auth.default()fallback (Cloud Run / WIF)"adc"The fallback only fires when nothing else registered, so keyed deployments aren’t disturbed. On Cloud Run with an attached SA, on GKE with workload identity, or on AWS via Workload Identity Federation, this is the path that boots the proxy without any JSON key on disk.
Returns the list of names actually registered by this call. Existing names are not overwritten unless
overwrite=True.Safe to call multiple times; failing sources are logged but don’t raise — discovery is best-effort.
- current() str[source]¶
Return the currently active tenant name. Falls back to the first registered name if no explicit
use()has been made yet and at least one credential is registered. Returns""if nothing’s registered.
- info(name: str | None = None) dict[source]¶
Inspect a registered credential without exposing the secret key material. Returns
{name, type, project_id, client_email, source}.name=Nonereturns the currently active one.
- get_token(name: str | None = None, force_refresh: bool = False) dict[source]¶
Mint (or return cached) access token for
name. If no name is given, uses the currently active tenant.Returns
{access_token, project_id, client_email, tenant}— same shape asgeeViz.eeAuth.registry.SARegistry.get_tokensobuild_proxy_routercan accept either.
- use(name: str)[source]¶
Switch the active credential and return a context manager that restores the previous one on exit. Works as a statement OR a
withblock:eeCreds.use("acme") # switch and forget ee.Image(1).getInfo() # uses acme with eeCreds.use("ian"): # scoped ee.Image(2).getInfo() # uses ian # back to acme here
- start(*, proxy_port: int = 8889, proxy_host: str = '127.0.0.1', ee_init: bool = True, launch_proxy: bool = True, workload_tag_builder=None) dict[source]¶
Initialize Earth Engine for multi-credential use.
Steps (each can be disabled via kwargs):
launch_proxy=True: start a background HTTP proxy that injects per-tenant SA / OAuth tokens. Required for switching credentials at runtime without re-initializingee.ee_init=True: callee.Initialize(url=proxy_url, ...)so the EE Python SDK routes all REST calls through the proxy.
Returns a status dict with
{started, proxy_url, tenants, ee_initialized}for inspection.Idempotent — calling
start()twice is safe; the second call returns the current state.
- stop() None[source]¶
Shut down the proxy — in-process AND any detached subprocess — and un-initialize the EE SDK so subsequent calls fail fast.
Without the SDK reset,
ee.Image(...).getInfo()afterstop()hangs on the socket timeout of the (now-dead) proxy URL before raising.ee.Reset()clears the SDK’s cached connection so the next call raises “Earth Engine client library not initialized” immediately.Prints one line describing what was torn down so the caller has a visible confirmation rather than a silent state change.
Safe to call when nothing is running.
- restart(*, mode: str = 'detached') dict[source]¶
Full
stop()followed byensure_started(mode=mode). Use after editing anygeeViz.eeAuthsource file so a fresh proxy loads the changes instead of the caller attaching to a still-running subprocess whose Python interpreter cached the old bytecode. Returns theensure_startedstatus dict.
- router(**kwargs)[source]¶
Return a FastAPI
APIRouterthat proxies EE requests using these credentials. Mount it in your own FastAPI app:app.include_router(eeCreds.router(), prefix="/ee-api")
kwargspass through tobuild_proxy_router— customize the tenant header, resolver, workload-tag builder, etc.
- classmethod stop_detached() bool[source]¶
Public helper: kill the detached proxy (if any) and clear the state file. Returns
Trueif a process was actually terminated,Falseif there was nothing to kill.
- property proxy_url: str | None¶
URL of the in-process proxy (if one’s running). Useful for embedding into iframe URLs / Map exports so the JS side uses the same proxy.
- sync_oauth_project(project: str) int[source]¶
Update every OAuth entry’s
project_idtoprojectand invalidate the cached access token so the next mint includes the new project on thex-goog-user-projectheader.Used by
robustInitializerafter the legacyee.Initializefallback succeeds: discovery may have guessed a project the OAuth user can’t access (e.g.gcloud configpointing at a service-account-owned project), but legacy init knows what ACTUALLY works. Syncing that back to the OAuth entries means subsequentMap.view()calls route through the proxy with the correct project instead of repeating the 403.Service-account entries are NOT touched — their project_id came from the SA JSON and is authoritative.
- Parameters:
project – The known-good project ID.
- Returns:
Number of entries actually updated.
- robust_init(*, verbose: bool = False, interactive: bool = True) dict[source]¶
Best-effort EE initialization with the simplest possible UX.
Decision tree (first hit wins, no prompts):
EE already initialized AND a test call works → return as-is.
eeAuth multi-tenant proxy via
ensure_started→ use it.ee.Initialize()with NO project arg → let EE’s own resolution chain (credentials’quota_project_id→ ADC → env vars) pick the project. This is the path that mirrors whatee.Initialize()would do if the user typed it themselves.Fallback:
ee.Authenticate(force=True, auth_mode='localhost')(interactive only) and re-run step 3.
No project-id prompts. If a user wants a specific project they can call
ee.Initialize(project='X')themselves before importing geeViz, or runearthengine set_project X/gcloud auth application-default set-quota-project X.Returns a status dict:
{"ok": bool, "source": "already-initialized" | "eeauth-proxy" | "ee-auto-init" | "interactive-auth", "project": "..."}
Raises
RuntimeErrorwhen no path completes — e.g. non-interactive environment with no creds, or no quota project discoverable after a fresh authenticate.- Parameters:
verbose – Print progress to stdout.
interactive – If False, skip the
ee.Authenticate()fallback and raise instead. Useful for daemons / CI where blocking on a browser would hang.
- ensure_started(*, mode: str = 'attached', proxy_port: int = 8889) dict[source]¶
Idempotent “I want the proxy running, please” helper used by
Map.view()and any other code that wants to ride the eeCreds proxy without forcing the user to calladdCreds+start.Modes:
"auto": try discovery + start (inline daemon thread). If anything fails, return a status dict withproxy_url=""— caller can fall back."proxy": try discovery + start (inline daemon thread). RAISE if nothing can be discovered or the proxy fails to bind."detached": attach to or spawn a long-lived background subprocess runningpython -m geeViz.eeAuth. Survives the calling script’s exit, so multi-Map.view()workflows and successive script invocations all share one proxy without needing the blockinginput()at the end of each. The subprocess is identified by a state file at<tmp>/.geeViz_eeauth_proxy.json; clients verify version + tenant fingerprint via/healthand respawn if anything drifted."legacy": do nothing. Returns immediately with"".
Returns
{proxy_url, tenants, current, mode, discovered}.proxy_url == ""means caller should fall back.