#!/usr/bin/env python """Retarget a frozen endpoint_diff manifest onto the transfer-ownership inputs. ``build_manifest.py`` produces a manifest whose requests carry the ``tests/integration/endpoints`` suite's own profiles and entity ids (the Frenchkiss / RIMAS / Bad Bunny accounts and their products). This script rewrites that manifest so every request instead runs against the fixed input set defined in ``tests/integration/transfer_ownership/conftest.py``: * **headers** -- each request is fanned out across the four baseline InsightsProfiles (employee, Thirty Tigers subaccount, RGE label, TVT label). The FF-enabled members of each pair are intentionally omitted: a flag-off profile is the production code path, which is what a before/after commit diff wants. * **path + query entity ids** -- product ids -> 956820, ISRCs -> USACQ0500001, (global) participant ids -> the artist uuid, UPCs -> 888880712318. Both the URL path and the query string are rewritten. * **account ids** -- ``/account//*`` and the ``account_id`` query param are retargeted to the account backing each profile (subaccount 7645 / RGE 21786 / TVT 15063), with ``account_type`` set to match; the employee, with full access, is fanned out across all four transfer accounts. The ``label_ids`` / ``subaccount_ids`` filter params name an account *type*, so they take the profile's accounts of that type -- never the wrong type -- and are dropped when it has none (a label profile has no subaccount). * **request bodies** -- a POST ``{"isrcs": [...]}`` body is collapsed to ``["USACQ0500001"]``. Date params (``start_date`` / ``end_date`` / ``days``) and ids with no transfer-suite analogue (video / channel / store ids) are left untouched -- a request that returns empty or 403 is still a valid before/after comparison. After retargeting, the fanned-out requests are deduped (a product-scoped request recorded against five different products collapses to one) and re-id'd. ``replay.py`` then fires this manifest unchanged. """ import argparse import datetime import json import os import re from collections import Counter from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit HERE = os.path.dirname(os.path.abspath(__file__)) # --------------------------------------------------------------------------- # Transfer-ownership inputs # (mirror tests/integration/transfer_ownership/conftest.py) # --------------------------------------------------------------------------- PROFILE_ID_HEADER = "Orchard-Profile-Id" PROFILE_TYPE_HEADER = "Orchard-Profile-Type" PROFILE_TYPE = "InsightsProfile" # (key, Orchard-Profile-Id) for the four baseline profiles. PROFILES = ( ("employee", "1100"), ("thirty_tigers_subaccount", "9900028"), ("rge_label", "9900030"), ("tvt_label", "9900032"), ) # profile key -> ((account_id, account_type), ...). Drives the per-profile # account fan-out for account-scoped requests (the employee, with full access, # spans all four transfer accounts) and -- by account type -- the label_ids / # subaccount_ids query-param retargeting. EMPLOYEE_ACCOUNTS = ( ("21989", "vendor"), # Thirty Tigers label ("7645", "subaccount"), # Thirty Tigers subaccount ("21786", "vendor"), # RGE label ("15063", "vendor"), # TVT label ) PROFILE_ACCOUNTS = { "employee": EMPLOYEE_ACCOUNTS, "thirty_tigers_subaccount": (("7645", "subaccount"),), "rge_label": (("21786", "vendor"),), "tvt_label": (("15063", "vendor"),), } PRODUCT_ID = "956820" ISRC = "USACQ0500001" GLOBAL_PARTICIPANT_ID = "ccf0617f-dd5a-49b9-90fc-c6c21fa8e012" UPC = "888880712318" # --------------------------------------------------------------------------- # Entity recognisers # --------------------------------------------------------------------------- # A 12-char alphanumeric path segment after /sound-recording/ is an ISRC; the # only other segments there ("streams", "aggregate-streams") never match. _ISRC_RE = re.compile(r"[A-Z0-9]{12}\Z") _UUID_RE = re.compile( r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}" r"-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\Z" ) # Every query param this script rewrites. account_id / account_type follow the # per-profile account context; label_ids / subaccount_ids are retargeted by # account type. Any param not listed -- dates, countries, store / video / # channel ids -- is left exactly as recorded. _RETARGET_QUERY_KEYS = { "product_id", "isrc", "track_isrcs", "global_participant_id", "global_participant_ids", "upc", "account_id", "account_type", "label_ids", "subaccount_ids", } # --------------------------------------------------------------------------- # Retargeting # --------------------------------------------------------------------------- def is_account_scoped(req): """True if the request views one specific account (``/account//*`` or an ``account_id`` query param). Such requests are fanned out, per profile, to that profile's backing account -- the employee profile across all four transfer accounts. ``label_ids`` / ``subaccount_ids`` are deliberately excluded: they name a filter list retargeted by account type, not a single viewed account. """ parts = urlsplit(req["url"]) segs = parts.path.split("/") if len(segs) >= 2 and segs[1] == "account": return True query_keys = {k for k, _ in parse_qsl(parts.query, keep_blank_values=True)} return "account_id" in query_keys def retarget_path(path, account_ctx): """Rewrite a product / sound-recording / participant / account id that sits as the second path segment (``///...``).""" segs = path.split("/") if len(segs) < 3: return path kind, entity = segs[1], segs[2] if kind == "product" and entity.isdigit(): segs[2] = PRODUCT_ID elif kind == "sound-recording" and _ISRC_RE.match(entity): segs[2] = ISRC elif kind == "participant" and _UUID_RE.match(entity): segs[2] = GLOBAL_PARTICIPANT_ID elif kind == "account" and entity.isdigit() and account_ctx: segs[2] = account_ctx[0] return "/".join(segs) def _retarget_query_value(key, value, account_ctx): """The retargeted value for a fixed-target query param. ``account_id`` / ``account_type`` follow the account context; ``label_ids`` / ``subaccount_ids`` are handled in retarget_query (they may be dropped). """ if key == "product_id": return PRODUCT_ID if key in ("isrc", "track_isrcs"): return ISRC if key in ("global_participant_id", "global_participant_ids"): return GLOBAL_PARTICIPANT_ID if key == "upc": return UPC if key == "account_id" and account_ctx: return account_ctx[0] if key == "account_type" and account_ctx: return account_ctx[1] return value def retarget_query(query, profile_key, account_ctx): """Rewrite entity-id query params, leaving every other param in place. ``label_ids`` / ``subaccount_ids`` name an account *type*, so they are retargeted to the profile's accounts of that type -- never the wrong type -- and dropped entirely when the profile has none (a label profile has no subaccount, and vice versa). """ if not query: return query vendor_accounts = [a for a, t in PROFILE_ACCOUNTS[profile_key] if t == "vendor"] subaccount_accounts = [ a for a, t in PROFILE_ACCOUNTS[profile_key] if t == "subaccount" ] out = [] collapsed = set() for key, value in parse_qsl(query, keep_blank_values=True): if key not in _RETARGET_QUERY_KEYS: out.append((key, value)) continue # An entity param may be repeated; rewrite it on first sight only. if key in collapsed: continue collapsed.add(key) if key == "label_ids": out.extend((key, acct) for acct in vendor_accounts) elif key == "subaccount_ids": out.extend((key, acct) for acct in subaccount_accounts) else: out.append((key, _retarget_query_value(key, value, account_ctx))) return urlencode(out) def retarget_body(body): """Collapse a POST ``{"isrcs": [...]}`` body to the single transfer ISRC. Other bodies (store_ids, video_ids, ...) have no transfer-suite analogue and are returned unchanged. """ if isinstance(body, dict) and isinstance(body.get("isrcs"), list): retargeted = dict(body) retargeted["isrcs"] = [ISRC] return retargeted return body def retarget_request(req, profile_key, profile_id, account_ctx): """Produce one retargeted request for a (profile, account) pairing.""" parts = urlsplit(req["url"]) headers = dict(req.get("headers") or {}) headers[PROFILE_ID_HEADER] = profile_id headers[PROFILE_TYPE_HEADER] = PROFILE_TYPE url = urlunsplit( ( parts.scheme, parts.netloc, retarget_path(parts.path, account_ctx), retarget_query(parts.query, profile_key, account_ctx), parts.fragment, ) ) return { "method": req["method"], "url": url, "headers": headers, "json": retarget_body(req.get("json")), "data": req.get("data"), } def expand(req): """Fan one recorded request out across the four transfer profiles. Account-scoped requests are additionally fanned out across the employee profile's four transfer accounts. """ account_scoped = is_account_scoped(req) for profile_key, profile_id in PROFILES: contexts = PROFILE_ACCOUNTS[profile_key] if account_scoped else (None,) for account_ctx in contexts: yield retarget_request(req, profile_key, profile_id, account_ctx) def request_key(req): """Stable identity of a request: method + url + headers + body.""" return json.dumps( { "method": req["method"], "url": req["url"], "headers": req.get("headers") or {}, "json": req.get("json"), "data": req.get("data"), }, sort_keys=True, default=str, ) def main(): ap = argparse.ArgumentParser( description="Retarget an endpoint_diff manifest onto the " "transfer-ownership profiles and entity ids." ) default_path = os.path.join(HERE, "_out", "manifest.json") ap.add_argument( "--in", dest="inp", default=default_path, help="source manifest (default: _out/manifest.json)", ) ap.add_argument( "--out", dest="outp", default=default_path, help="destination manifest (default: _out/manifest.json)", ) args = ap.parse_args() with open(args.inp) as fh: manifest = json.load(fh) source = manifest.get("requests", []) if not source: raise SystemExit(f"no requests in {args.inp}") deduped = {} expanded = 0 for req in source: for variant in expand(req): expanded += 1 deduped.setdefault(request_key(variant), variant) ordered = sorted( deduped.values(), key=lambda r: ( r["method"], r["url"], json.dumps(r["headers"], sort_keys=True), json.dumps(r["json"], sort_keys=True, default=str), ), ) width = max(5, len(str(len(ordered) - 1))) requests_out = [ {"id": f"{idx:0{width}d}", **req} for idx, req in enumerate(ordered) ] now = datetime.datetime.now(datetime.timezone.utc).isoformat() out = { "generated_at": now, "git_rev": manifest.get("git_rev", "?"), "retargeted": True, "retargeted_from": { "source": os.path.basename(args.inp), "unique_requests": len(source), }, "unique_requests": len(requests_out), "requests": requests_out, } with open(args.outp, "w") as fh: json.dump(out, fh, indent=2, sort_keys=True) # ---- summary ----------------------------------------------------------- by_profile = Counter(r["headers"][PROFILE_ID_HEADER] for r in requests_out) key_by_id = {pid: key for key, pid in PROFILES} account_scoped = sum(1 for r in source if is_account_scoped(r)) print(f"source manifest : {args.inp}") print(f"source requests : {len(source)} ({account_scoped} account-scoped)") print(f"expanded variants : {expanded}") print(f"unique after dedup : {len(requests_out)}") print("per-profile breakdown:") for pid, count in sorted(by_profile.items()): print(f" {key_by_id.get(pid, pid):<26} (id {pid}): {count}") print(f"wrote {args.outp}") if __name__ == "__main__": main()