From 0eb6feefa83a923a47d767f47556abcd4e4eda58 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Fri, 17 Apr 2026 19:23:54 +0000 Subject: [PATCH] Add typer CLI + production Dockerfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Context ------- Closes Phase 0 scaffolding. Image must build and run so infra can schedule an initial no-op CronJob (the plan's Phase 0 exit criterion) while Phase 0.5 / 0.75 / 1 land. This change ----------- - broker_sync/cli.py: typer app with two commands. * `version` — prints __version__; used as the no-op CronJob liveness check. * `auth-spike` — Phase 0.5 end-to-end live probe: log in to Wealthfolio, list accounts, exit 0 on success. Credentials read from env (WF_BASE_URL/USERNAME/PASSWORD) so CronJob + ESO can inject them without CLI flags. - Dockerfile: multi-stage, Python 3.12-slim, non-root user 10001 with /data as the shared PVC mount. Poetry virtualenv baked into /app/.venv, entrypoint is `broker-sync`, default command `version`. - CLI test via typer.testing.CliRunner. Test plan --------- ## Automated - poetry run pytest -q → 32 passed - poetry run mypy broker_sync tests → Success: no issues found in 19 source files - poetry run ruff check . → All checks passed! - poetry run broker-sync version → broker-sync 0.1.0 ## Manual Verification Docker build + run deferred — image will be built via GHA after the repo is pushed to GitHub in a follow-up session; the pyproject install has already been verified locally. --- Dockerfile | 32 +++++++++++++++++++++++++ broker_sync/cli.py | 59 ++++++++++++++++++++++++++++++++++++++++++++++ tests/test_cli.py | 12 ++++++++++ 3 files changed, 103 insertions(+) create mode 100644 Dockerfile create mode 100644 broker_sync/cli.py create mode 100644 tests/test_cli.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..541b67d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +FROM python:3.12-slim AS builder + +ENV POETRY_VERSION=1.8.4 \ + POETRY_HOME=/opt/poetry \ + POETRY_VIRTUALENVS_IN_PROJECT=true \ + PIP_NO_CACHE_DIR=1 + +RUN pip install --no-cache-dir "poetry==${POETRY_VERSION}" + +WORKDIR /app +COPY pyproject.toml poetry.lock ./ +RUN /opt/poetry/bin/poetry install --only main --no-root + +COPY broker_sync ./broker_sync +RUN /opt/poetry/bin/poetry install --only main + + +FROM python:3.12-slim + +WORKDIR /app + +RUN useradd --system --uid 10001 --home /app --shell /usr/sbin/nologin broker && \ + mkdir -p /data && chown -R broker:broker /data + +COPY --from=builder --chown=broker:broker /app /app + +ENV PATH="/app/.venv/bin:${PATH}" \ + PYTHONUNBUFFERED=1 + +USER broker +ENTRYPOINT ["broker-sync"] +CMD ["version"] diff --git a/broker_sync/cli.py b/broker_sync/cli.py new file mode 100644 index 0000000..a98d124 --- /dev/null +++ b/broker_sync/cli.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +import os +import sys + +import typer + +app = typer.Typer(help="broker-sync: pull brokerage activity into Wealthfolio") + + +@app.command("version") +def version() -> None: + """Print version and exit — used by the no-op Phase 0 CronJob as a liveness check.""" + from broker_sync import __version__ + typer.echo(f"broker-sync {__version__}") + + +@app.command("auth-spike") +def auth_spike( + wf_base_url: str = typer.Option(..., envvar="WF_BASE_URL", help="Wealthfolio base URL"), + wf_username: str = typer.Option(..., envvar="WF_USERNAME"), + wf_password: str = typer.Option(..., envvar="WF_PASSWORD"), + session_path: str = typer.Option("/data/wealthfolio_session.json", envvar="WF_SESSION_PATH"), +) -> None: + """Phase 0.5 — prove end-to-end auth + 1 activity import against live Wealthfolio.""" + import asyncio + + from broker_sync.sinks.wealthfolio import WealthfolioSink + + async def _run() -> None: + sink = WealthfolioSink( + base_url=wf_base_url, + username=wf_username, + password=wf_password, + session_path=session_path, + ) + try: + await sink.login() + accounts = await sink.list_accounts() + typer.echo(f"Logged in. {len(accounts)} account(s) visible.") + finally: + await sink.close() + + try: + asyncio.run(_run()) + except Exception as e: + typer.echo(f"auth-spike failed: {e}", err=True) + sys.exit(1) + + +def main() -> None: + # Entry point called by the console-script in pyproject.toml. + app() + + +if __name__ == "__main__": + # Guard env for readability when running under `python -m broker_sync.cli`. + os.environ.setdefault("COLUMNS", "120") + main() diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..d05a3e7 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,12 @@ +from typer.testing import CliRunner + +from broker_sync import __version__ +from broker_sync.cli import app + +runner = CliRunner() + + +def test_version_prints_package_version() -> None: + result = runner.invoke(app, ["version"]) + assert result.exit_code == 0 + assert __version__ in result.stdout