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— theput/lookupProtocol every store implements. Implementations must be thread-safe and idempotent onput.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:
putwrites 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?”.lookupreads 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
|
Join sanitized parts with |
Return the process-wide default store. |
|
|
Deterministic short tag from a parts dict + secret. |
Sanitize a single component of a workload tag. |
Classes
|
Write to the first store; read from all of them in order. |
Process-local dict-backed store. |
|
|
File-backed store using |
|
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 adigest_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
TagStorethat recordstag → partsat mint time so lookups can recover identity later.
- class geeViz.eeAuth.tags.TagStore(*args, **kwargs)[source]¶
Bases:
ProtocolMinimal 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).
- class geeViz.eeAuth.tags.ChainedTagStore(*stores)[source]¶
Bases:
objectWrite 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 bareNonethat 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.
- class geeViz.eeAuth.tags.InMemoryTagStore[source]¶
Bases:
objectProcess-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.
- class geeViz.eeAuth.tags.SQLiteTagStore(path: str | Path | None = None)[source]¶
Bases:
objectFile-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.