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: object

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()
setTagStore(store) EECreds[source]

Replace the default TagStore used for auto-minted workload tags. Pass an InMemoryTagStore(), SQLiteTagStore(path=...), or any object matching the TagStore protocol. Call this BEFORE start() 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:

  1. tag = mint_workload_tag(parts, secret=…) — deterministic short hash (wl_<hex>).

  2. getTagStore().put(tag, parts) — so lookupWorkloadTag(tag) can recover parts later, and any Cloud Monitoring row tagged with tag can be joined back to identity.

  3. 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 by getMapId, 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 None if 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 to addImpersonation() so the proxy mints tokens via the IAM credentials API at request time (no key held)

Auto-detects which credential shape it is. project overrides the default project — required for OAuth + impersonation, optional for SAs since their JSON has project_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_email at request time.

No key material is held. The runtime’s ADC source must hold roles/iam.serviceAccountTokenCreator on the target SA. On each token mint, google.auth.impersonated_credentials calls 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.

list() list[str][source]

Return registered credential names in insertion order.

names() list[str][source]

Alias for list() — also returns registered names.

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.

has(name: str) bool[source]
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=None returns 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 as geeViz.eeAuth.registry.SARegistry.get_token so build_proxy_router can 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 with block:

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):

  1. launch_proxy=True: start a background HTTP proxy that injects per-tenant SA / OAuth tokens. Required for switching credentials at runtime without re-initializing ee.

  2. ee_init=True: call ee.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() after stop() 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 by ensure_started(mode=mode). Use after editing any geeViz.eeAuth source 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 the ensure_started status dict.

router(**kwargs)[source]

Return a FastAPI APIRouter that proxies EE requests using these credentials. Mount it in your own FastAPI app:

app.include_router(eeCreds.router(), prefix="/ee-api")

kwargs pass through to build_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 True if a process was actually terminated, False if 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_id to project and invalidate the cached access token so the next mint includes the new project on the x-goog-user-project header.

Used by robustInitializer after the legacy ee.Initialize fallback succeeds: discovery may have guessed a project the OAuth user can’t access (e.g. gcloud config pointing at a service-account-owned project), but legacy init knows what ACTUALLY works. Syncing that back to the OAuth entries means subsequent Map.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):

  1. EE already initialized AND a test call works → return as-is.

  2. eeAuth multi-tenant proxy via ensure_started → use it.

  3. 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 what ee.Initialize() would do if the user typed it themselves.

  4. 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 run earthengine 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 RuntimeError when 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 call addCreds + start.

Modes:

  • "auto": try discovery + start (inline daemon thread). If anything fails, return a status dict with proxy_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 running python -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 blocking input() 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 /health and respawn if anything drifted.

  • "legacy": do nothing. Returns immediately with "".

Returns {proxy_url, tenants, current, mode, discovered}. proxy_url == "" means caller should fall back.