File size: 7,950 Bytes
37d92f0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | """ig_master.py β the PLATFORM-WIDE Instagram master store (wave 22, contract C6 / ruling R2).
WHY A MASTER AT ALL. History is unbuyable: no vendor sells "followers on 1 January", so the
series starts the day capture starts β and a profile two tenants both track was, until this
wave, two separate paid pulls writing two private copies. R2 pools the SNAPSHOT HISTORY on the
operator plane: every pull WRITE-THROUGHS here as well as to the tenant's own `ut_ig_*` tables,
and the time-window metric fields (C7) read THIS series β so a handle any tenant tracks has one
continuous history, and nobody re-bills for what the platform already knows.
RESIDENCY (C6 + amendment): its OWN private dataset repo β `royal-imports/aios-ig-master`, env
`AIOS_IG_MASTER_REPO` β reached through the `core/store.py::handle()` seam so R9's Postgres
flip carries it unchanged. β NEVER the tenant `user_tables` bucket: that bucket serialises
whole (MEASURED 35.8 MB at the append-table ceiling), so parking a platform-wide series inside
any tenant's bucket would tax every unrelated write in that tenant. Three buckets of its own:
master_snapshots append key `<handle>@<pulled_at>` profile counts over time
master_posts upsert key `<shortcode>` post identity (handle, when)
master_post_snapshots append key `<shortcode>@<pulled_at>` engagement over time
TENANT ATTRIBUTION IS FOR AUDIT/ERASURE ONLY (C6, verbatim): every row carries the tenant slug
that paid for the pull, and NOTHING reads it to answer a data question β the pooling benefit
is exactly that reads are attribution-blind. D-24's subject purge walks these buckets too
(`purge_handle`), because a right-to-erasure that missed the pooled copy would not be erasure.
FAILURE POSTURE: three states, never conflated. `off` = no repo configured on this deployment
(local dev, the gates) β the tenant copy is the whole story and the run says so WITHOUT going
partial; `ok` = written; `error` = configured and the write failed β a LOUD partial in the run
summary, never a silent skip (C6, verbatim).
"""
from __future__ import annotations
import os
ENV_REPO = "AIOS_IG_MASTER_REPO"
SNAP_BUCKET = "master_snapshots"
POST_BUCKET = "master_posts"
PSNAP_BUCKET = "master_post_snapshots"
#: One bucket's row ceiling. The append tables grow forever by design; the cap exists so a
#: runaway writer is a LOUD partial long before a bucket becomes unserialisable. At ~1k
#: profiles/day this is ~16 months of profile snapshots β re-derive before raising.
MASTER_MAX_ROWS = 500_000
def repo_id():
return (os.environ.get(ENV_REPO) or "").strip()
def configured():
return bool(repo_id())
def _handle():
"""The store handle β THE seam (C6): `core.store.handle` picks the backend, so the pg flip
(R9) moves this store the day it moves every other one. Monkeypatched by the gate."""
import core.store as store
return store.handle(repo_id=repo_id(), slug="ig_master")
def _upsert():
# Lazy: the engine imports this module, so a top-level engine import would be a cycle.
import automation_engine as engine
return engine.upsert_rows
def append_run(tenant, snaps, posts, psnaps):
"""Write-through one run's accumulated rows. Returns `(status, note)`,
status β ok | off | error β see the module header for what each obliges the caller to say.
One coalesced update PER BUCKET per run (the 256-commits/hr law travels β this repo has its
own budget, and a per-row writer would spend it the same way). Empty inputs cost nothing.
"""
if not configured():
return "off", "no master store on this deployment β tenant copy only"
slug = str(tenant or "").strip() or "unknown"
stamped = {
SNAP_BUCKET: ([{**r, "tenant": slug} for r in snaps or []], "snapshot_key"),
POST_BUCKET: ([{**r, "tenant": slug} for r in posts or []], "shortcode"),
PSNAP_BUCKET: ([{**r, "tenant": slug} for r in psnaps or []], "post_snapshot_key"),
}
upsert = _upsert()
capped = []
try:
h = _handle()
for bucket, (rows_in, key_field) in stamped.items():
if not rows_in:
continue
existing = dict(h.get(bucket) or {})
merged, counts = upsert(existing, rows_in, key_field, cap=MASTER_MAX_ROWS)
if counts.get("capped"):
capped.append(f"{bucket} is FULL ({MASTER_MAX_ROWS:,}) β "
f"{counts['capped']} rows lost")
h.update(bucket, lambda cur, m=merged: m, flush="sync")
except Exception as e: # noqa: BLE001
return "error", f"the master store did not take the write ({type(e).__name__})"
if capped:
return "error", "; ".join(capped)
return "ok", ""
def series_for(handles):
"""The C7 read seam: `{handle: {snapshots: [...], posts: [...], postSnapshots:
{shortcode: [...]}}}` for the handles asked about, each series sorted ascending by its
time key. Attribution-blind by design (the pooling benefit). `{}` when unconfigured β
a metric over no master reads BLANK, never zero."""
if not configured():
return {}
want = {str(x or "").strip().lower() for x in handles or [] if str(x or "").strip()}
if not want:
return {}
try:
h = _handle()
snaps = h.get(SNAP_BUCKET) or {}
posts = h.get(POST_BUCKET) or {}
psnaps = h.get(PSNAP_BUCKET) or {}
except Exception: # noqa: BLE001
return {}
out = {w: {"snapshots": [], "posts": [], "postSnapshots": {}} for w in want}
code_owner = {}
for r in posts.values():
hkey = str((r or {}).get("influencer_key") or "").strip().lower()
if hkey in out:
out[hkey]["posts"].append(dict(r))
code_owner[str(r.get("shortcode") or "")] = hkey
for r in snaps.values():
hkey = str((r or {}).get("influencer_key") or "").strip().lower()
if hkey in out:
out[hkey]["snapshots"].append(dict(r))
for r in psnaps.values():
code = str((r or {}).get("shortcode") or "")
hkey = code_owner.get(code)
if hkey:
out[hkey]["postSnapshots"].setdefault(code, []).append(dict(r))
for w in out.values():
w["snapshots"].sort(key=lambda r: str(r.get("pulled_at") or ""))
w["posts"].sort(key=lambda r: str(r.get("posted_at") or ""))
for series in w["postSnapshots"].values():
series.sort(key=lambda r: str(r.get("pulled_at") or ""))
return out
def purge_handle(handle):
"""D-24's master half: remove EVERY row about one subject from all three buckets.
Returns `{bucket: removed}`; `{}` when unconfigured (nothing exists to purge). The tenant
half lives in the engine (`purge_subject`) β the two run together, and the gate proves a
purged handle leaves no row on either side."""
if not configured():
return {}
subject = str(handle or "").strip().lower()
if not subject:
return {}
h = _handle()
removed = {}
posts = h.get(POST_BUCKET) or {}
codes = {str(r.get("shortcode") or "") for r in posts.values()
if str((r or {}).get("influencer_key") or "").strip().lower() == subject}
def _sweep(bucket, keep):
cur = h.get(bucket) or {}
nxt = {k: v for k, v in cur.items() if keep(v)}
removed[bucket] = len(cur) - len(nxt)
if removed[bucket]:
h.update(bucket, lambda c, m=nxt: m, flush="sync")
_sweep(SNAP_BUCKET,
lambda r: str((r or {}).get("influencer_key") or "").strip().lower() != subject)
_sweep(POST_BUCKET,
lambda r: str((r or {}).get("influencer_key") or "").strip().lower() != subject)
_sweep(PSNAP_BUCKET, lambda r: str((r or {}).get("shortcode") or "") not in codes)
return removed
|