PROP OUT looks in this weekend's Location Assets folder, read on NA Ingest

- prop_bundle_folder takes {date} (the plan's weekend, as the tracks folders): any bundle there counts
- readiness scripts for this Mac run here (NA ProP's Google Drive showed 261025 empty)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 14:40:18 -04:00
co-authored by Claude Opus 5.5
parent 42b325e927
commit ee7326a6eb
4 changed files with 45 additions and 13 deletions
+2 -2
View File
@@ -935,8 +935,8 @@
"pco_module": "pco-plan", "pco_module": "pco-plan",
"pco_service_type": "275974", "pco_service_type": "275974",
"projects": "~/Library/CloudStorage/GoogleDrive-media@waterschurch.org/Shared drives/Waters Creative/Ableton/Projects", "projects": "~/Library/CloudStorage/GoogleDrive-media@waterschurch.org/Shared drives/Waters Creative/Ableton/Projects",
"prop_bundle_folder": "~/Library/CloudStorage/GoogleDrive-media@waterschurch.org/Shared drives/Location Assets/ProPresenter", "prop_bundle_folder": "~/Library/CloudStorage/GoogleDrive-media@waterschurch.org/Shared drives/Location Assets/{date}",
"prop_bundle_host": "prop", "prop_bundle_host": "ingest",
"prop_bundle_pattern": "*.probundle", "prop_bundle_pattern": "*.probundle",
"prop_host": "prop", "prop_host": "prop",
"prop_lists": [ "prop_lists": [
+23 -7
View File
@@ -137,9 +137,11 @@ def ssh_argv(cfg, host):
def run_remote(cfg, host, script, timeout=60): def run_remote(cfg, host, script, timeout=60):
"""Run a Python script on another Mac (on stdin, at low priority); its last line is JSON.""" """Run a Python script on another Mac (on stdin, at low priority), or here when `host` is this Mac
r = subprocess.run(ssh_argv(cfg, host) + ["nice -n 19 /usr/bin/python3 -"], input=script, (NA Ingest's Google Drive is current; NA ProP's lagged a day behind, 2026-10-21); its last line is JSON."""
capture_output=True, text=True, timeout=timeout) here = ((cfg.get("hosts") or {}).get(host) or {}).get("self")
argv = ["/usr/bin/nice", "-n", "19", "/usr/bin/python3", "-"] if here else ssh_argv(cfg, host) + ["nice -n 19 /usr/bin/python3 -"]
r = subprocess.run(argv, input=script, capture_output=True, text=True, timeout=timeout)
if r.returncode == 255: if r.returncode == 255:
raise ReadyError("%s UNREACHABLE" % host.upper()) raise ReadyError("%s UNREACHABLE" % host.upper())
lines = [ln for ln in r.stdout.strip().splitlines() if ln.strip()] lines = [ln for ln in r.stdout.strip().splitlines() if ln.strip()]
@@ -971,8 +973,21 @@ print(json.dumps(sorted([[os.path.basename(f), os.path.getmtime(f)] for f in fil
''' '''
def eval_prop_bundle(rc, files, since): def prop_bundle_folder(rc, plan):
"""Where this weekend's bundle goes: "{date}" is the weekend's folder (Location Assets/261025, the
user 2026-10-21: "it should be looking at the latest weekends folder in location assets")."""
return str(rc.get("prop_bundle_folder", "")).replace("{date}", datetime.fromtimestamp(plan["time"]).strftime("%y%m%d"))
def eval_prop_bundle(rc, files, since, folder=None):
line = Line() line = Line()
if "{date}" in str(rc.get("prop_bundle_folder", "")):
# This weekend's own folder: any bundle in it is this weekend's.
if not files:
line.fault(ERR, "NOT IN " + os.path.basename(str(folder or "").rstrip("/")), "no %s in %s" % (rc.get("prop_bundle_pattern"), folder))
else:
line.info = "SENT " + datetime.fromtimestamp(files[0][1]).strftime("%a %H:%M").upper()
return line
fresh = [f for f in files if f[1] >= since] fresh = [f for f in files if f[1] >= since]
if not fresh: if not fresh:
last = datetime.fromtimestamp(files[0][1]).strftime("%-m/%-d") if files else "NEVER" last = datetime.fromtimestamp(files[0][1]).strftime("%-m/%-d") if files else "NEVER"
@@ -1021,8 +1036,8 @@ def look(cfg, now=None, data_dir=None, sources=None):
resolume=lambda: http_json(rc.get("resolume_url", "").rstrip("/") + "/api/v1/composition", timeout=6), resolume=lambda: http_json(rc.get("resolume_url", "").rstrip("/") + "/api/v1/composition", timeout=6),
macros=lambda paths: run_remote(cfg, rc["prop_host"], MACRO_SCRIPT % json.dumps({"paths": paths}), timeout=60), macros=lambda paths: run_remote(cfg, rc["prop_host"], MACRO_SCRIPT % json.dumps({"paths": paths}), timeout=60),
companion=lambda n: companion_page(cfg, n), companion=lambda n: companion_page(cfg, n),
prop_bundle=lambda: run_remote(cfg, rc["prop_bundle_host"], BUNDLE_SCRIPT % json.dumps( prop_bundle=lambda plan: run_remote(cfg, rc["prop_bundle_host"], BUNDLE_SCRIPT % json.dumps(
{"folder": rc.get("prop_bundle_folder", ""), "pattern": rc.get("prop_bundle_pattern", "*.probundle")}), timeout=30)) {"folder": prop_bundle_folder(rc, plan), "pattern": rc.get("prop_bundle_pattern", "*.probundle")}), timeout=30))
src.update(sources or {}) src.update(sources or {})
lines = {} lines = {}
@@ -1103,7 +1118,8 @@ def look(cfg, now=None, data_dir=None, sources=None):
lines["mics"] = mics lines["mics"] = mics
if rc.get("prop_bundle_host"): if rc.get("prop_bundle_host"):
try: try:
lines["prop_bundle"] = eval_prop_bundle(rc, src["prop_bundle"](), week_start(now, rc.get("reset_day", "mon"))) lines["prop_bundle"] = eval_prop_bundle(rc, src["prop_bundle"](plan), week_start(now, rc.get("reset_day", "mon")),
prop_bundle_folder(rc, plan))
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
failed(["prop_bundle"], e) failed(["prop_bundle"], e)
out = {"time": now, "items": {k: v.as_dict() for k, v in lines.items()}} out = {"time": now, "items": {k: v.as_dict() for k, v in lines.items()}}
+2 -1
View File
@@ -399,7 +399,8 @@ field("ready.resolume_deck", "str", "", "The deck Resolume must be on.", "Ready"
field("ready.prop_host", "str", None, "The ProPresenter computer: its macros and presentation files are read to find " field("ready.prop_host", "str", None, "The ProPresenter computer: its macros and presentation files are read to find "
"the Resolume columns Sunday's slides call up.", "Ready") "the Resolume columns Sunday's slides call up.", "Ready")
field("ready.prop_bundle_host", "str", None, "The computer that saves the ProPresenter bundle.", "Ready") field("ready.prop_bundle_host", "str", None, "The computer that saves the ProPresenter bundle.", "Ready")
field("ready.prop_bundle_folder", "path", "", "Where the bundle for the locations goes.", "Ready") field("ready.prop_bundle_folder", "path", "", "Where the bundle for the locations goes. {date} is this weekend's folder "
"(Location Assets/{date} -> Location Assets/261025): any bundle in it counts as sent.", "Ready")
field("ready.prop_bundle_pattern", "str", "*.probundle", "The bundle's file name pattern.", "Ready", advanced=True) field("ready.prop_bundle_pattern", "str", "*.probundle", "The bundle's file name pattern.", "Ready", advanced=True)
field("ready.vocal_positions", "list", ["Vocalist", "[Leader]"], "PCO positions that need a vocal mic.", "Ready") field("ready.vocal_positions", "list", ["Vocalist", "[Leader]"], "PCO positions that need a vocal mic.", "Ready")
field("ready.mic_positions", "dict", {}, "Other PCO positions and the mic they need (MC -> MC).", "Ready") field("ready.mic_positions", "dict", {}, "Other PCO positions and the mic they need (MC -> MC).", "Ready")
+18 -3
View File
@@ -338,10 +338,19 @@ class TestLineupCrossBundle(unittest.TestCase):
def test_the_macro_reader_runs(self): def test_the_macro_reader_runs(self):
compile(rd.MACRO_SCRIPT % json.dumps({"paths": ["/x.pro"]}), "remote", "exec") compile(rd.MACRO_SCRIPT % json.dumps({"paths": ["/x.pro"]}), "remote", "exec")
def test_prop_bundle_in_this_weekends_folder(self):
rc = dict(RC, prop_bundle_folder="~/Shared drives/Location Assets/{date}")
plan = {"time": datetime(2026, 10, 25, 9, 0).timestamp()}
folder = rd.prop_bundle_folder(rc, plan)
self.assertEqual(folder, "~/Shared drives/Location Assets/261025")
self.assertEqual(msgs(rd.eval_prop_bundle(rc, [["WORSHIP BUNDLE.probundle", WEDNESDAY - 86400 * 10]], WEDNESDAY, folder)), [])
self.assertEqual(msgs(rd.eval_prop_bundle(rc, [], WEDNESDAY, folder)), ["NOT IN 261025"])
def test_prop_bundle_this_week(self): def test_prop_bundle_this_week(self):
since = rd.week_start(WEDNESDAY) since = rd.week_start(WEDNESDAY)
self.assertEqual(msgs(rd.eval_prop_bundle(RC, [["Message Templates.probundle", WEDNESDAY - 3600]], since)), []) rc = dict(RC, prop_bundle_folder="~/Shared drives/Location Assets/ProPresenter") # one folder for every week
self.assertEqual(msgs(rd.eval_prop_bundle(RC, [["Old.probundle", since - 86400]], since)), ["NOT SENT (LAST 10/11)"]) self.assertEqual(msgs(rd.eval_prop_bundle(rc, [["Message Templates.probundle", WEDNESDAY - 3600]], since)), [])
self.assertEqual(msgs(rd.eval_prop_bundle(rc, [["Old.probundle", since - 86400]], since)), ["NOT SENT (LAST 10/11)"])
def test_mics_from_pco(self): def test_mics_from_pco(self):
self.assertEqual(rd.mics_needed(RC, plan()), "2 VOX MC TIM") self.assertEqual(rd.mics_needed(RC, plan()), "2 VOX MC TIM")
@@ -351,7 +360,7 @@ class TestLook(unittest.TestCase):
def sources(self, **over): def sources(self, **over):
src = dict(pco=plan, prop=lambda p: prop(), ableton=lambda p: ableton(), src = dict(pco=plan, prop=lambda p: prop(), ableton=lambda p: ableton(),
lineup=lambda: lineup(["Jonah Perdue", "Krista Perdue", "Kevin Michel", "Anthony Hoyt", "Tim Hatch"]), lineup=lambda: lineup(["Jonah Perdue", "Krista Perdue", "Kevin Michel", "Anthony Hoyt", "Tim Hatch"]),
resolume=composition, prop_bundle=lambda: [["Message Templates.probundle", WEDNESDAY]], resolume=composition, prop_bundle=lambda plan: [["Message Templates.probundle", WEDNESDAY]],
macros=macros_found, companion=lambda n: COMPANION.get(n, {})) macros=macros_found, companion=lambda n: COMPANION.get(n, {}))
src.update(over) src.update(over)
return src return src
@@ -609,3 +618,9 @@ class TestOtherCampuses(unittest.TestCase):
finally: finally:
rd.http_json, rd.pco_get, rd.pco_login = real_http, real_get, real_login rd.http_json, rd.pco_get, rd.pco_login = real_http, real_get, real_login
self.assertEqual(len([c for c in calls if "/1113969/" in c[1] and c[0] == "POST"]), len(res["songs"])) # one note per location song self.assertEqual(len([c for c in calls if "/1113969/" in c[1] and c[0] == "POST"]), len(res["songs"])) # one note per location song
class TestRunHere(unittest.TestCase):
def test_a_script_for_this_mac_runs_here_without_ssh(self):
cfg = {"hosts": {"ingest": {"self": True}}}
self.assertEqual(rd.run_remote(cfg, "ingest", "import json; print(json.dumps({'ok': 1}))"), {"ok": 1})