geeViz.eeAuth.client¶
Client-side helpers for routing the Earth Engine Python SDK through a token-injecting proxy.
The pattern:
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.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.
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
Email of the user the current EE call is being made on behalf of. |
|
Session the current EE call belongs to — the companion to |
|
ContextVar holding the current tenant id. |
|
Billing tenant slug — distinct from |
|
Action label for the current EE call (e.g. "map", "compute", "thumb", "export"). |
Functions
|
Initialize the Earth Engine SDK to route all REST calls through |
|
Restore the tenant to what it was before the matching |
|
Set the current tenant for subsequent EE calls in this context. |
|
Scoped tenant switch. |
Classes
|
|
- 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_proxyin a plain-Python setup) from the same workload-tag builder the agent uses.TenantAwareHttpreads 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 intoanonymousin 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.
TenantAwareHttpreads 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 toCURRENT_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_buildercan emit that as theactionpart 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_tenantto restore the previous value. Prefertenant_context()for scoped use.
- geeViz.eeAuth.client.reset_tenant(token) None[source]¶
Restore the tenant to what it was before the matching
set_tenantcall.- Parameters:
token – The token object returned by the paired
set_tenant()call. Passing a token from a different scope raisesValueError.
- 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:
objecthttplib2.Httpsubclass that stamps the tenant header on every outbound request and strips whatever Authorization the SDK injected.Subclassed at first instantiation so
httplib2is 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.Httpis NOT thread-safe — its per-host connection cache (self.connections) is a plaindictmutated from insiderequest()andsocket.HTTPConnectionobjects hold per-instance socket state. When the EE SDK shares one transport across aThreadPoolExecutor(asMap.testLayers()does with 8 workers), concurrent threads tear down each other’s sockets mid-request, surfacing as'NoneType' object has no attribute 'close'and WindowsWinError 10038/10057socket errors.Workaround: route each thread’s
request()call to its OWNhttplib2.Httpinstance stored inthreading.local(). EE’s SDK only consults the transport forrequest()— it doesn’t reach intoself.connectionsdirectly — so per-thread instances are a safe drop-in.Attribution headers¶
Besides the tenant header,
request()stamps theX-Agent-*attribution headers built from this module’s four ContextVars — but only whenGEEVIZ_AGENT_ATTRIB_SECRETis set in the environment, in which case its value goes out asX-Agent-Attrib-Secretto 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-apipath treats apparently-loopback callers as trusted — a judgement the client can influence viaX-Forwarded-Forwhen uvicorn runs withforwarded_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
AnonymousCredentialssince the proxy holds the real SA credentials. The SDK’s bearer-token header is stripped byTenantAwareHttpbefore 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-CredsmatchesgeeViz.eeAuth.server.project – Placeholder project id passed to
ee.Initialize(EE requires one but the proxy overrides per-tenant viax-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.Initializeor surface the error). Prints any underlying exception to stderr; doesn’t re-raise.