"""Find (and delete) objects that harness runs left in A4H: names with a run prefix at the start (Z4CGT1HJ_X) or inside (ZCL_Z4CGT1HJ_X). python3 -m harness.sweep dry run: list them by prefix python3 -m harness.sweep --delete delete them (ADT deletion API, dependency order); a locked object is reported, not forced python3 -m harness.sweep --keep-recent 60 do not touch prefixes of runs whose directory changed in the last 60 minutes (default 60) Never run it while a controller or a series is writing: it can only judge by directory age. """ import argparse import glob import json import os import re import time from .adt_client import load_env from .mcp_client import McpClient from .runner import DELETE_ORDER, delete_uris from .task import prefix_for ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) START = re.compile(r"^(Z\d[0-9A-Z]{6}_)", re.I) MID = re.compile(r"^[A-Z]{1,5}_(Z\d[0-9A-Z]{6}_)", re.I) def recent_prefixes(minutes): keep = set() for d in glob.glob(os.path.join(ROOT, "runs", "**", "*_G*_*"), recursive=True) + glob.glob(os.path.join(ROOT, "runs", "**", "*_T*_*"), recursive=True): b = os.path.basename(d) m = re.match(r"^(\d+)_([GT]\d+)_", b) if m and time.time() - os.path.getmtime(d) < minutes * 60: keep.add(prefix_for(int(m.group(1)), m.group(2)).upper()) return keep def main(): load_env(os.path.join(ROOT, ".env")) ap = argparse.ArgumentParser() ap.add_argument("--delete", action="store_true") ap.add_argument("--keep-recent", type=int, default=60) a = ap.parse_args() found = {} with McpClient() as m: # run prefixes are Z + a digit + 6 characters: one query per digit (a single "Z*" query is cut at 1000 results), at the start and after the type part for q in [f"Z{d}*" for d in "0123456789"] + [f"*_Z{d}*" for d in "0123456789"] + ["ZPROBE*", "ZTEST*"]: try: for o in json.loads(m.call("sap_search_object", {"query": q, "maxResults": 1000})[1]): if o.get("objectType"): # a STOB entry of a CDS view has the same name and no objectType: skip it found[(o["name"], o["objectType"])] = o except ValueError: pass keep = recent_prefixes(a.keep_recent) by_prefix = {} for (n, _t), o in found.items(): mm = START.match(n.upper()) or MID.match(n.upper()) if not mm or not o.get("objectType"): continue pre = mm.group(1).upper() if len(pre) != 9 or not pre[1].isdigit(): continue if pre in keep: continue by_prefix.setdefault(pre, []).append(o) total = sum(len(v) for v in by_prefix.values()) print(f"{total} objects of {len(by_prefix)} run prefixes ({len(keep)} recent prefixes kept)") for pre, objs in sorted(by_prefix.items()): print(" ", pre, [o["name"] for o in objs][:6]) json.dump({p: [o["name"] for o in v] for p, v in by_prefix.items()}, open(os.path.join(ROOT, "runs", "sweep.json"), "w"), indent=1) if not a.delete: return objs = sorted([o for v in by_prefix.values() for o in v], key=lambda o: DELETE_ORDER.index(o["objectType"]) if o["objectType"] in DELETE_ORDER else 99) done = failed = 0 for i in range(0, len(objs), 10): uris = [] for o in objs[i:i + 10]: uris.append(o["uri"]) res = delete_uris(uris) for u, v in res.items(): if v["deleted"]: done += 1 else: failed += 1 print("not deleted:", u.split("/")[-1], v["msg"]) print(f"deleted {done}, not deleted {failed}") if __name__ == "__main__": main()