geeViz.eeAuth.threadctx

Carry attribution context across thread boundaries.

Why this exists

EE attribution rides on ContextVar``s. ``TenantAwareHttp reads CURRENT_USER_EMAIL / CURRENT_SESSION_ID / CURRENT_TENANT on every outbound EE request and stamps them as X-Agent-* headers; the ee-proxy resolves identity from those headers and mints the workload tag that Cloud Monitoring bills against.

ContextVar``s do **not** cross ``threading.Thread boundaries. A thread starts with an empty context regardless of what the spawning thread had set. That is normally harmless, but geeViz runs EE work in threads constantly — run_code execs user code in one, printEE spawns one per call, map/chart helpers spawn more — so the identity set for a tool call evaporated the moment the work moved off the calling thread. Every such call reached the proxy with no identity headers and was logged via=ANONYMOUS user=anonymous sub=- sess=-, then billed to nobody.

Wrapping the thread target with run_in_context() makes the child thread run inside a snapshot of the spawning thread’s context, so the ContextVars are visible and the headers get stamped.

Usage

Take the snapshot on the spawning thread, at spawn time:

from geeViz.eeAuth.threadctx import run_in_context
t = threading.Thread(target=run_in_context(work), args=(x,))
t.start()

One wrapper per thread

contextvars.Context.run() refuses re-entry — calling it on a Context that is already executing raises RuntimeError. Each call to run_in_context() takes its own snapshot, so a wrapper is good for exactly one thread. For a pool, wrap per task rather than reusing one wrapper across workers:

pool.map(lambda a: run_in_context(work)(a), items)   # per task

Deliberately stdlib-only, with no geeViz imports, so any module can import it without risking a cycle.

Functions

context_thread(target, *[, args, kwargs, ...])

threading.Thread that inherits the caller's context.

run_in_context(fn)

Wrap fn so it runs inside a snapshot of the CALLER's context.

geeViz.eeAuth.threadctx.run_in_context(fn: Callable[[...], _T]) → Callable[[...], _T][source]

Wrap fn so it runs inside a snapshot of the CALLER’s context.

The snapshot is taken here, on the calling thread — not inside the wrapper — because by the time the wrapper runs it is already on the new thread, whose context is empty. Capturing late is the whole bug this function exists to avoid.

The returned callable is single-use per thread; see the module docstring.

geeViz.eeAuth.threadctx.context_thread(target: Callable[[...], Any], *, args: tuple = (), kwargs: dict | None = None, daemon: bool | None = None, name: str | None = None) → Thread[source]

threading.Thread that inherits the caller’s context.

Convenience over Thread(target=run_in_context(fn), ...). Returns an unstarted thread so the caller keeps control of start().