geeViz.eeAuth.client

Client-side helpers for routing the Earth Engine Python SDK through a token-injecting proxy.

The pattern:

  1. Run a proxy server (see geeViz.eeAuth.server) that holds the SA credentials and substitutes the right bearer token per request based on a tenant header / query param.

  2. Tell the EE SDK to send all REST calls through that proxy instead of directly to Google. Pass anonymous credentials — the proxy supplies the real ones.

  3. Switch tenants on the client side by setting a ContextVar; the custom HTTP transport reads it and stamps the routing header on every outbound request.

That gives you full multi-tenant concurrency in a single Python process, which the bare EE SDK can’t do because ee.Initialize() stores credentials in module-global state.

Quick start

from geeViz.eeAuth import initialize_via_proxy, tenant_context
import ee

initialize_via_proxy("http://localhost:8888/ee-api")
# Now ee.X calls go through the proxy with the default tenant

with tenant_context("training"):
    ee.Image(1).getInfo()  # uses the training SA

Attribution context

Routing says which credential pays; attribution says who to charge it to. Four ContextVars carry that second answer to the proxy: CURRENT_USER_EMAIL, CURRENT_SESSION_ID, CURRENT_ACTION and CURRENT_BILLING_TENANT. Whatever is set when an EE call goes out is stamped onto the outbound request as X-Agent-* headers, and the proxy’s workload-tag builder folds those parts into the tag it mints — so the resulting EE spend can be traced back to a real person, session and operation.

Setting them is optional and additive. A standalone geeViz install leaves all four empty and gets exactly the historic behavior: no attribution headers, and a tag built from credential/pid/source alone. They exist for hosts that already know the caller’s identity (an agent’s MCP tool wrapper, a web backend) and would otherwise see every programmatic EE call — thumbnails, GIFs, charts, zonal stats — collapse into anonymous, because those requests carry no browser Referer, session cookie or IAP header to identify them by.

GEEVIZ_AGENT_ATTRIB_SECRET

The attribution headers are sent only when the GEEVIZ_AGENT_ATTRIB_SECRET environment variable is set, and the value is sent alongside them as X-Agent-Attrib-Secret. The gate is a security boundary, not a convenience toggle: those headers name who gets billed for EE compute, and the proxy’s /ee-api path skips auth for callers that appear to be on loopback. Under forwarded_allow_ips="*" “appears to be loopback” is a client-controlled judgement — a remote caller can present as loopback via X-Forwarded-For — so an ungated header would let anyone bill their usage to a victim’s account. The server that spawns geeViz puts the secret in the child’s environment; when it is absent we send no attribution headers at all and the call attributes to nobody, which is the safe failure.

Module Attributes

CURRENT_USER_EMAIL

Email of the user the current EE call is being made on behalf of.

CURRENT_SESSION_ID

Session the current EE call belongs to — the companion to CURRENT_USER_EMAIL, stamped by the same code path.

CURRENT_TENANT

ContextVar holding the current tenant id.

CURRENT_BILLING_TENANT

Billing tenant slug — distinct from CURRENT_TENANT (which is the eeCreds credential-name, e.g. "ee-persistent" for ADC or "acme-prod" for a named SA).

CURRENT_ACTION

Action label for the current EE call (e.g. "map", "compute", "thumb", "export").

Functions

initialize_via_proxy(proxy_url[, ...])

Initialize the Earth Engine SDK to route all REST calls through proxy_url.

reset_tenant(token)

Restore the tenant to what it was before the matching set_tenant call.

set_tenant(tenant)

Set the current tenant for subsequent EE calls in this context.

tenant_context(tenant)

Scoped tenant switch.

Classes

TenantAwareHttp([tenant_header])

httplib2.Http subclass that stamps the tenant header on every outbound request and strips whatever Authorization the SDK injected.

geeViz.eeAuth.client.CURRENT_USER_EMAIL: ContextVar = <ContextVar name='current_user_email' default=''>

Email of the user the current EE call is being made on behalf of. Propagated from the calling agent to the outbound EE proxy: set by the MCP tool wrapper (or the caller of initialize_via_proxy in a plain-Python setup) from the same workload-tag builder the agent uses. TenantAwareHttp reads it on every outbound request and stamps it as an HTTP header so the proxy’s own workload-tag builder can attribute the call to the right user even when the request has no browser Referer, session cookie, or IAP header (i.e., every MCP-generated EE call — thumb, gif, chart, zonal stats, etc.). Without this thread-through, those calls all collapsed into anonymous in Cloud Monitoring and were invisible in the per-user spend table.

geeViz.eeAuth.client.CURRENT_SESSION_ID: ContextVar = <ContextVar name='current_session_id' default=''>

Session the current EE call belongs to — the companion to CURRENT_USER_EMAIL, stamped by the same code path. Lets spend be sliced per conversation/run, not just per user. Empty means no session known; the tag is still minted, just without a session part.

geeViz.eeAuth.client.CURRENT_TENANT: ContextVar = <ContextVar name='geeViz_ee_tenant' default=''>

ContextVar holding the current tenant id. TenantAwareHttp reads it on every outbound EE REST call and stamps the routing header. Empty string means “use the proxy’s default tenant” (no header sent).

geeViz.eeAuth.client.CURRENT_BILLING_TENANT: ContextVar = <ContextVar name='geeViz_billing_tenant' default=''>

Billing tenant slug — distinct from CURRENT_TENANT (which is the eeCreds credential-name, e.g. “ee-persistent” for ADC or “acme-prod” for a named SA). Agents set this to their fixed deploy-time tenant (e.g. “askterra”) so the workload-tag mint carries the right attribution regardless of which cred is currently active. Empty means no separate billing tenant known — the default builder falls back to CURRENT_TENANT (matches standalone-geeViz behavior).

geeViz.eeAuth.client.CURRENT_ACTION: ContextVar = <ContextVar name='geeViz_ee_action' default=''>

Action label for the current EE call (e.g. “map”, “compute”, “thumb”, “export”). Set by the caller so _default_workload_tag_builder can emit that as the action part instead of a generic “proxy-default” marker. Empty means no action known — builder still mints, just without an action label.

geeViz.eeAuth.client.set_tenant(tenant: str)[source]

Set the current tenant for subsequent EE calls in this context.

Returns a token that can be passed to reset_tenant to restore the previous value. Prefer tenant_context() for scoped use.

geeViz.eeAuth.client.reset_tenant(token) → None[source]

Restore the tenant to what it was before the matching set_tenant call.

Parameters:

token – The token object returned by the paired set_tenant() call. Passing a token from a different scope raises ValueError.

geeViz.eeAuth.client.tenant_context(tenant: str)[source]

Scoped tenant switch.

with tenant_context("training"):
    ee.Image(1).getInfo()
# back to previous tenant here
class geeViz.eeAuth.client.TenantAwareHttp(tenant_header: str = 'X-geeViz-Creds')[source]

Bases: object

httplib2.Http subclass that stamps the tenant header on every outbound request and strips whatever Authorization the SDK injected.

Subclassed at first instantiation so httplib2 is only imported when actually used — keeps unit tests that mock the EE init path from needing the dependency. The header name is set per-instance so you can run multiple proxies with different conventions in the same process if you really need to.

Thread safety

httplib2.Http is NOT thread-safe — its per-host connection cache (self.connections) is a plain dict mutated from inside request() and socket.HTTPConnection objects hold per-instance socket state. When the EE SDK shares one transport across a ThreadPoolExecutor (as Map.testLayers() does with 8 workers), concurrent threads tear down each other’s sockets mid-request, surfacing as 'NoneType' object has no attribute 'close' and Windows WinError 10038/10057 socket errors.

Workaround: route each thread’s request() call to its OWN httplib2.Http instance stored in threading.local(). EE’s SDK only consults the transport for request() — it doesn’t reach into self.connections directly — so per-thread instances are a safe drop-in.

Attribution headers

Besides the tenant header, request() stamps the X-Agent-* attribution headers built from this module’s four ContextVars — but only when GEEVIZ_AGENT_ATTRIB_SECRET is set in the environment, in which case its value goes out as X-Agent-Attrib-Secret to prove the headers came from a process the server itself spawned. Those headers decide who gets billed for EE compute and who has CDU caps consumed, and the proxy’s /ee-api path treats apparently-loopback callers as trusted — a judgement the client can influence via X-Forwarded-For when uvicorn runs with forwarded_allow_ips="*". Ungated, that would let a remote caller bill their usage to anyone they can name. With no secret present we send no attribution headers and the call attributes to nobody, which is the safe failure. See the module docstring for the full rationale.

geeViz.eeAuth.client.initialize_via_proxy(proxy_url: str, tenant_header: str = 'X-geeViz-Creds', project: str | None = None, origin: str = '') → bool[source]

Initialize the Earth Engine SDK to route all REST calls through proxy_url.

Uses AnonymousCredentials since the proxy holds the real SA credentials. The SDK’s bearer-token header is stripped by TenantAwareHttp before reaching the proxy anyway.

Parameters:
  • proxy_url – Base URL of the EE proxy, e.g. "http://localhost:8888/ee-api". No trailing slash.

  • tenant_header – Header name the proxy expects for tenant routing. Default X-geeViz-Creds matches geeViz.eeAuth.server.

  • project – Placeholder project id passed to ee.Initialize (EE requires one but the proxy overrides per-tenant via x-goog-user-project). Default "ee-proxy-placeholder".

  • origin – Human-readable provenance label appended to the “EE initialized via proxy” line printed on stderr — today "spawned new detached proxy" or "attached to existing detached proxy". Purely diagnostic; it changes no behavior. Default "" omits the label entirely. Worth passing: without it the line reads identically whether a proxy was started or merely reused, which is how a kill-and-respawn on a different port looked like “two proxies running” in a notebook.

Returns:

True on success, False if init failed (caller should fall back to direct ee.Initialize or surface the error). Prints any underlying exception to stderr; doesn’t re-raise.