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/
|
.venv/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
figs/
|
figs/
|
||||||
|
|||||||
@ -1,3 +1,7 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["uv_build>=0.5.0,<0.9"]
|
||||||
|
build-backend = "uv_build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "dimension-reduction-sandbox"
|
name = "dimension-reduction-sandbox"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
@ -23,6 +27,17 @@ dependencies = [
|
|||||||
"sse-starlette>=2.1.3",
|
"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]
|
[tool.uv]
|
||||||
override-dependencies = [
|
override-dependencies = [
|
||||||
"annoy ; sys_platform == 'never'", # block source build of annoy; annoy-mm provides the module
|
"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())
|
||||||
2
uv.lock
generated
2
uv.lock
generated
@ -747,7 +747,7 @@ wheels = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "dimension-reduction-sandbox"
|
name = "dimension-reduction-sandbox"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
source = { virtual = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "annoy-mm" },
|
{ name = "annoy-mm" },
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user