dr-sandbox: thin CLI package with 'setup' for rendering the Prefect compose stack

This commit is contained in:
Michael Pilosov 2026-04-23 14:52:26 -06:00
parent 879f7d662d
commit 6816c95e27
6 changed files with 235 additions and 1 deletions

3
.gitignore vendored
View File

@ -1,4 +1,7 @@
.venv/
__pycache__/
*.egg-info/
dist/
build/
figs/

View File

@ -1,3 +1,7 @@
[build-system]
requires = ["uv_build>=0.5.0,<0.9"]
build-backend = "uv_build"
[project]
name = "dimension-reduction-sandbox"
version = "0.0.1"
@ -23,6 +27,17 @@ dependencies = [
"sse-starlette>=2.1.3",
]
[project.scripts]
dr-sandbox = "dr_sandbox.cli:main"
# Only the thin CLI shim is a distributable package. `app/`, `flows/`,
# `scripts/` stay source-tree-local — they're run in-place, not installed.
# Because the project slug ("dimension-reduction-sandbox") differs from
# the importable module, tell uv_build where to look.
[tool.uv.build-backend]
module-name = "dr_sandbox"
module-root = "src"
[tool.uv]
override-dependencies = [
"annoy ; sys_platform == 'never'", # block source build of annoy; annoy-mm provides the module

View File

View File

@ -0,0 +1,98 @@
# Prefect 3 self-hosted stack.
# Generated by `dr-sandbox setup` — re-run to regenerate with different values.
# Pinned to a known stable patch release; bump deliberately, not via `latest`.
x-prefect-image: &prefect-image prefecthq/prefect:@@IMAGE@@
x-prefect-env: &prefect-env
PREFECT_API_DATABASE_CONNECTION_URL: postgresql+asyncpg://prefect:@@PASSWORD@@@postgres:5432/prefect
PREFECT_API_DATABASE_MIGRATE_ON_START: "true"
PREFECT_SERVER_ANALYTICS_ENABLED: "false"
DO_NOT_TRACK: "1"
PREFECT_LOGGING_LEVEL: INFO
PREFECT_MESSAGING_BROKER: prefect_redis.messaging
PREFECT_MESSAGING_CACHE: prefect_redis.messaging
PREFECT_REDIS_MESSAGING_HOST: redis
PREFECT_REDIS_MESSAGING_PORT: "6379"
PREFECT_REDIS_MESSAGING_DB: "0"
services:
postgres:
image: postgres:16-alpine
restart: always
environment:
POSTGRES_USER: prefect
POSTGRES_PASSWORD: @@PASSWORD@@
POSTGRES_DB: prefect
volumes:
- prefect_postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U prefect -d prefect"]
interval: 5s
timeout: 5s
retries: 10
networks: [prefect]
redis:
image: redis:7-alpine
restart: always
# No AOF/RDB persistence: Prefect uses Redis as an ephemeral
# messaging broker + cache — restarts re-emit from the server.
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 10
networks: [prefect]
prefect-server:
image: *prefect-image
restart: always
depends_on:
postgres: { condition: service_healthy }
redis: { condition: service_healthy }
environment:
<<: *prefect-env
PREFECT_SERVER_API_HOST: 0.0.0.0
PREFECT_SERVER_API_PORT: "4200"
# Browser-facing URL; override at runtime via the env var of the same name.
PREFECT_SERVER_UI_API_URL: ${PREFECT_SERVER_UI_API_URL:-@@UI_API_URL@@}
PREFECT_UI_ENABLED: "true"
command: ["prefect", "server", "start", "--no-services", "--host", "0.0.0.0", "--port", "4200"]
ports:
- "@@PORT@@:4200"
expose:
- "4200"
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0) if urllib.request.urlopen('http://localhost:4200/api/health', timeout=2).status==200 else sys.exit(1)"]
interval: 30s
timeout: 5s
retries: 5
start_period: 30s
networks: [prefect]
prefect-services:
image: *prefect-image
restart: always
depends_on:
prefect-server: { condition: service_healthy }
environment:
<<: *prefect-env
PREFECT_API_URL: http://prefect-server:4200/api
command: ["prefect", "server", "services", "start"]
networks: [prefect]
volumes:
prefect_postgres:
networks:
prefect:
name: prefect
# Other stacks can attach to this network to reach the API at
# http://prefect-server:4200/api — add to the other compose file:
#
# networks:
# prefect:
# external: true
# name: prefect
#

118
src/dr_sandbox/cli.py Normal file
View File

@ -0,0 +1,118 @@
"""dr-sandbox CLI — one-shot helpers for setting up this project.
For now, just `dr-sandbox setup`: renders the Prefect stack
docker-compose.yml into a destination path. Prompts for each value (enter
accepts the default shown in brackets). Pass --auto to take every default
without prompting.
"""
from __future__ import annotations
import argparse
import sys
from importlib import resources
from pathlib import Path
from typing import Dict, List, Optional
DEFAULT_OUTPUT = "./prefect/docker-compose.yml"
DEFAULTS: Dict[str, str] = {
"IMAGE": "3.6.27-python3.12", # Prefect docker image tag
"PORT": "4200", # host-exposed port -> 4200 inside
"PASSWORD": "prefect", # postgres password
"UI_API_URL": "http://localhost:4200/api",
}
def _prompt(label: str, default: str) -> str:
try:
resp = input(f" {label} [{default}]: ").strip()
except EOFError:
return default
return resp or default
def _render(values: Dict[str, str]) -> str:
tmpl = (
resources.files("dr_sandbox")
.joinpath("assets/docker-compose.yml.tmpl")
.read_text(encoding="utf-8")
)
out = tmpl
for k, v in values.items():
out = out.replace(f"@@{k}@@", v)
return out
def _cmd_setup(args: argparse.Namespace) -> int:
if args.auto:
out_path = Path(DEFAULT_OUTPUT)
values = dict(DEFAULTS)
else:
print("prefect stack config — press enter to accept each default\n")
out_path = Path(_prompt("output path", DEFAULT_OUTPUT))
values = {k: _prompt(_LABELS[k], DEFAULTS[k]) for k in DEFAULTS}
out_path = out_path.expanduser().resolve()
if out_path.exists() and not args.force:
print(
f"refusing to overwrite {out_path} — pass --force to replace",
file=sys.stderr,
)
return 1
rendered = _render(values)
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(rendered, encoding="utf-8")
print()
print(f"wrote {out_path}")
print()
print("next steps:")
print(f" (cd {out_path.parent} && docker compose up -d)")
print(f" uv sync # install python deps in .venv")
print(f" make service-install # optional: run the worker as a systemd user service")
return 0
_LABELS: Dict[str, str] = {
"IMAGE": "prefect image tag",
"PORT": "host-exposed API port",
"PASSWORD": "postgres password",
"UI_API_URL": "browser-facing API URL",
}
def main(argv: Optional[List[str]] = None) -> int:
ap = argparse.ArgumentParser(
prog="dr-sandbox",
description="dr-sandbox helpers — today, just the first-time-setup flow.",
)
sub = ap.add_subparsers(dest="cmd", required=True)
p_setup = sub.add_parser(
"setup",
help="render the Prefect docker-compose.yml for this project",
description=(
"Render the Prefect stack docker-compose.yml. Prompts for each "
"knob (image tag, port, postgres password, UI URL); press enter "
"to accept the default shown. Use --auto for non-interactive."
),
)
p_setup.add_argument(
"--auto", action="store_true",
help="take every default without prompting",
)
p_setup.add_argument(
"--force", action="store_true",
help="overwrite the output file if it already exists",
)
p_setup.set_defaults(fn=_cmd_setup)
args = ap.parse_args(argv)
return args.fn(args)
if __name__ == "__main__":
sys.exit(main())

2
uv.lock generated
View File

@ -747,7 +747,7 @@ wheels = [
[[package]]
name = "dimension-reduction-sandbox"
version = "0.0.1"
source = { virtual = "." }
source = { editable = "." }
dependencies = [
{ name = "annoy-mm" },
{ name = "fastapi" },