dr-sandbox: thin CLI package with 'setup' for rendering the Prefect compose stack
This commit is contained in:
parent
879f7d662d
commit
6816c95e27
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,4 +1,7 @@
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
|
||||
figs/
|
||||
|
||||
@ -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
|
||||
|
||||
0
src/dr_sandbox/__init__.py
Normal file
0
src/dr_sandbox/__init__.py
Normal file
98
src/dr_sandbox/assets/docker-compose.yml.tmpl
Normal file
98
src/dr_sandbox/assets/docker-compose.yml.tmpl
Normal 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
118
src/dr_sandbox/cli.py
Normal 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())
|
||||
Loading…
Reference in New Issue
Block a user