geeViz.eeAuth.tags

Earth Engine workload-tag helpers.

EE workload tags surface in GCP Billing under the goog-earth-engine-workload-tag label, so tagged calls can be broken down per user / session / source. This module builds well-formed tags from arbitrary string parts (sanitizing each so the result is always accepted by EE).

EE constraints (from ee/_state.py validation):

  • 1 - 63 characters

  • begins and ends with a lowercase alphanumeric [a-z0-9]

  • middle characters: [a-z0-9_-] (lowercase alphanumeric, dash, underscore)

No uppercase, no ., no other punctuation. Anything outside that set (@, spaces, slashes, dots, uppercase, etc.) gets sanitized to -.

Separator: ``__`` (double underscore). Single - already appears inside sanitized parts (e.g. ihousman-redcastleresources-com), so we reserve double underscore as the between-parts delimiter. That makes tags trivially parseable with tag.split("__"):

agent__run_code__ihousman-redcastleresources-com__db208a06-1c49

To keep __ an unambiguous separator, runs of _ within a part get collapsed to a single _ during sanitization (so an input like run__code becomes run_code). Underscores from sources like tool names — run_code, map_control — pass through intact because they’re already singletons.

Tag stores

63 characters isn’t enough to spell out a real identity tuple (user email + session id + action + tenant), so mint_workload_tag() hashes the parts into a short wl_<hex> tag instead. That tag is not reversible on its own — recovering who a billing row belongs to needs the tag -> parts mapping written down somewhere. That “somewhere” is this module’s store layer:

  • TagStore — the put / lookup Protocol every store implements. Implementations must be thread-safe and idempotent on put.

  • InMemoryTagStore — process-local dict. Fine for one-shot scripts and tests; the mapping dies with the process.

  • SQLiteTagStore — single file, typically ~/.geeViz/workload_tags.db. Survives kernel restarts and reruns. Not suitable for multi-instance deployments, where each instance would keep its own file and miss the others’ tags.

  • ChainedTagStore — several stores treated as one.

  • default_tag_store() — the store used when nobody configured one.

ChainedTagStore is the newest piece and the least obvious, because its two operations deliberately disagree about scope:

  • put writes to the primary (first) store only. Attribution needs exactly one writer; fanning writes out to every backend would create diverging partial copies and turn a lookup miss into “which copy is right?”.

  • lookup reads from every backend in order, first hit wins (a failing backend is logged and skipped rather than breaking the chain). Reads have no such problem — tags are content-addressed, so the same parts and secret always produce the same tag and it does not matter which backend answered.

That asymmetry exists because more than one store can be live on a machine at once — an agent writing to Postgres beside a notebook using the sqlite default. Without chaining, a tag minted under one is simply invisible to the other’s lookup, and the caller gets a bare None that reads as “never minted” when the truth is “you asked the wrong store”.

Functions

build_workload_tag(*parts)

Join sanitized parts with __ and clamp to EE's 63-char limit.

default_tag_store()

Return the process-wide default store.

mint_workload_tag(parts, *, secret[, ...])

Deterministic short tag from a parts dict + secret.

sanitize_workload_tag_part(s)

Sanitize a single component of a workload tag.

Classes

ChainedTagStore(*stores)

Write to the first store; read from all of them in order.

InMemoryTagStore()

Process-local dict-backed store.

SQLiteTagStore([path])

File-backed store using sqlite3 (stdlib, no extra deps).

TagStore(*args, **kwargs)

Minimal contract for tag → parts persistence.

geeViz.eeAuth.tags.sanitize_workload_tag_part(s: str) → str[source]

Sanitize a single component of a workload tag.

  • Lowercases.

  • Replaces disallowed characters with -.

  • Collapses runs of - to a single -.

  • Collapses runs of _ to a single _ so the __ separator stays unambiguous when parts are joined.

  • Strips leading/trailing - and _ (EE rejects tags that don’t begin and end with an alphanumeric).

geeViz.eeAuth.tags.build_workload_tag(*parts: str) → str[source]

Join sanitized parts with __ and clamp to EE’s 63-char limit.

Empty / falsy parts are dropped. The final tag is guaranteed to satisfy EE’s regex: [a-z0-9][a-z0-9_\-]{0,61}[a-z0-9]. Returns an empty string if everything was dropped — callers should treat empty as “no tag” and skip the workload-tag header / body field entirely.

geeViz.eeAuth.tags.mint_workload_tag(parts: dict[str, Any], *, secret: str, digest_size: int = 8) → str[source]

Deterministic short tag from a parts dict + secret.

Returns wl_<hex> where the hex is a digest_size-byte blake2b of the canonicalized parts (default 16 hex chars → collision probability ~10⁻⁹ at millions of tags). Same input always yields the same tag.

The tag is NOT reversible on its own — pair with a TagStore that records tag → parts at mint time so lookups can recover identity later.

class geeViz.eeAuth.tags.TagStore(*args, **kwargs)[source]

Bases: Protocol

Minimal contract for tag → parts persistence.

Implementations MUST be thread-safe (the proxy calls concurrently from request handlers) and idempotent on put (mint is deterministic — re-inserting the same (tag, parts) is a no-op, not an error).

put(tag: str, parts: dict[str, Any]) → None[source]
lookup(tag: str) → dict[str, Any] | None[source]
class geeViz.eeAuth.tags.ChainedTagStore(*stores)[source]

Bases: object

Write to the first store; read from all of them in order.

Exists because more than one store can be live on the same machine. A geeViz agent installs a Postgres-backed store, while notebooks and standalone scripts use the sqlite default — so a tag minted under one is invisible to the other’s lookup, and the caller sees a bare None that reads as “this tag was never minted” when the truth is “you asked the wrong store”. That is how EE usage ends up labeled unattributed while its mapping sits intact a few directories away.

The asymmetry is deliberate:

  • put -> primary only. Writing to every backend would make two partial, diverging copies of the mapping and turn a lookup miss into a correctness question (“which one is right?”). Attribution needs exactly one writer.

  • lookup -> every backend, in order, first hit wins. Reads are free of that problem: finding a mapping somewhere is strictly better than not finding it, and the answer is the same wherever it came from because tags are content-addressed (same parts + same secret = same tag).

A failing backend never breaks the chain — it’s logged and skipped, so an unreachable Postgres degrades to “sqlite still answers” instead of taking lookups down with it.

Example:

eeCreds.setTagStore(ChainedTagStore(PostgresTagStore(...),
                                    SQLiteTagStore()))
property primary

The store that receives writes.

put(tag: str, parts: dict[str, Any]) → None[source]
lookup(tag: str) → dict[str, Any] | None[source]
class geeViz.eeAuth.tags.InMemoryTagStore[source]

Bases: object

Process-local dict-backed store.

Fast, zero-dependency, but the mapping dies with the Python process and doesn’t cross processes/instances. Use for one-shot scripts and unit tests; use SQLiteTagStore (or a Postgres impl) for anything that outlives the process.

put(tag: str, parts: dict[str, Any]) → None[source]
lookup(tag: str) → dict[str, Any] | None[source]
class geeViz.eeAuth.tags.SQLiteTagStore(path: str | Path | None = None)[source]

Bases: object

File-backed store using sqlite3 (stdlib, no extra deps).

Survives kernel restarts and re-runs of the same script. Single-file, typically at ~/.geeViz/workload_tags.db. Suitable for notebooks and single-instance CLIs; NOT suitable for multi-instance Cloud Run (each instance would have its own file, cross-instance lookups would silently miss). Use a Postgres impl for that.

Concurrent read/write across threads in one process is safe (sqlite3 connection is created per-call with a short-lived cursor). Multiple processes sharing the same file also work (sqlite handles the file lock), though heavy write contention will degrade.

put(tag: str, parts: dict[str, Any]) → None[source]
lookup(tag: str) → dict[str, Any] | None[source]
geeViz.eeAuth.tags.default_tag_store() → TagStore[source]

Return the process-wide default store. First call constructs SQLiteTagStore() at ~/.geeViz/workload_tags.db. Callers who want in-memory or a custom store should set it explicitly via eeCreds.setTagStore(...) before eeCreds.start().