Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ ENV PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
UV_COMPILE_BYTECODE=1 \
UV_NO_CACHE=1 \
UV_PYTHON_INSTALL_DIR=/opt/python \
PATH="/app/.venv/bin:${PATH}" \
PORT=8080

Expand All @@ -24,4 +25,6 @@ RUN python -m pip install --upgrade pip uv \

USER appuser

CMD ["gunicorn", "--bind", ":8080", "main:app"]
RUN /app/.venv/bin/python -m scripts.publish_digest_before_read --help

CMD ["/app/.venv/bin/python", "-m", "gunicorn", "--bind", ":8080", "main:app"]
4 changes: 4 additions & 0 deletions docs/unified-daily-notification.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ monitoring checks, but run the archive-only publisher independently in an
approved Cloud Run Job using the platform image and its frozen dependencies.

Command: `/app/.venv/bin/python -m scripts.publish_digest_before_read`.
Use `--help` for an offline CLI check; it exits before publication or the watchdog.
The image keeps uv-managed Python under `/opt/python` so `appuser` can traverse
its path. Build checks and runtime commands use `/app/.venv/bin/python` directly;
the Docker build also checks publisher help after switching to `appuser`.
The command imports the existing producer, never the service entrypoint. It does
not run a drill, refresh a broker session, invoke a trading route, send Telegram,
or access Secret Manager. Archive reads use the Storage SDK and the job's
Expand Down
5 changes: 4 additions & 1 deletion scripts/publish_digest_before_read.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Publish one completed-window candidate before the central read, without broker calls."""
from __future__ import annotations

import argparse
import json
import os
import signal
Expand Down Expand Up @@ -114,7 +115,9 @@ def publish(env, *, now=None, client=None, emit=None):
return {"status": "published", "business_day": day, "readback_verified": True}


def main():
def main(argv=None):
argparse.ArgumentParser(description=__doc__).parse_args(argv)

def timeout(signum, frame):
raise DeadlineExceeded()

Expand Down
20 changes: 17 additions & 3 deletions tests/test_publish_digest_before_read.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ def test_main_redacts_exception_and_clears_watchdog(monkeypatch, capsys):
monkeypatch.setattr(publisher.signal, "signal", lambda *a: None)
monkeypatch.setattr(publisher.signal, "alarm", alarms.append)
monkeypatch.setattr(publisher, "publish", lambda *a: (_ for _ in ()).throw(RuntimeError("private-value")))
assert publisher.main() == 1
assert publisher.main([]) == 1
output = capsys.readouterr().out
assert "private-value" not in output
assert json.loads(output)["status"] == "failed"
Expand All @@ -185,7 +185,7 @@ def emit(*a):
except Exception:
pytest.fail("deadline must escape producer catch-all")
monkeypatch.setattr(publisher, "publish", emit)
assert publisher.main() == 1
assert publisher.main([]) == 1
assert json.loads(capsys.readouterr().out)["status"] == "failed"


Expand All @@ -212,7 +212,7 @@ def fail(*a):
monkeypatch.setattr(publisher, "publish", fail)
monkeypatch.setattr(publisher.signal, "signal", lambda *a: None)
monkeypatch.setattr(publisher.signal, "alarm", lambda *a: None)
assert publisher.main() == 1
assert publisher.main([]) == 1
output = capsys.readouterr().out
assert "private resource" not in output
assert json.loads(output)["stage"] == stage
Expand Down Expand Up @@ -250,3 +250,17 @@ def test_invalid_runtime_identity_is_rejected_before_idle_projection(key, value)
with pytest.raises(ValueError, match="target_invalid"):
publisher.publish({**ENV, "RUNTIME_TARGET_JSON": json.dumps(target)}, now=lambda: NOW,
emit=lambda *a, **kw: pytest.fail("must not project an invalid target"))


@pytest.mark.parametrize("args, exit_code", [(["--help"], 0), (["--unknown"], 2)])
def test_cli_arguments_exit_before_publication_or_watchdog(args, exit_code, monkeypatch, capsys):
def unexpected(*a, **kw):
pytest.fail("CLI argument handling must not start publication or the watchdog")
monkeypatch.setattr(publisher, "publish", unexpected)
monkeypatch.setattr(publisher.signal, "signal", unexpected)
monkeypatch.setattr(publisher.signal, "alarm", unexpected)
with pytest.raises(SystemExit) as caught:
publisher.main(args)
assert caught.value.code == exit_code
output = capsys.readouterr()
assert "usage:" in (output.out if exit_code == 0 else output.err)
Loading