MIL-002: backend basics and Hello Flask #33

Merged
Tirsvad merged 5 commits from mil-002-hello-flask into main 2026-11-06 05:36:43 +01:00
Showing only changes of commit 3845fb028f - Show all commits
+214 -212
View File
@@ -1,212 +1,214 @@
"""Create the lecture screenshots in docs/images/ without touching the desktop. """Create the lecture screenshots in docs/images/ without touching the desktop.
Every screenshot is taken by a headless Edge or Chrome (no window, no mouse, no Every screenshot is taken by a headless Edge or Chrome (no window, no mouse, no
keyboard). Terminal screenshots render the real output of real commands. keyboard). Terminal screenshots render the real output of real commands.
Usage (from the repository root, with the project's .venv active): Usage (from the repository root, with the project's .venv active):
python docs/images/capture.py 1 2 3 4 5 6 python docs/images/capture.py 1 2 3 4 5 6
""" """
# Developer tool only (not shipped in the package): every command it runs is a fixed list
import html # built from constants, sys.executable and local paths; it reads no external input.
import os
import shutil import html
import subprocess import os
import sys import shutil
import tempfile import subprocess
import time import sys
import urllib.request import tempfile
from pathlib import Path import time
import urllib.request
ROOT = Path(__file__).resolve().parents[2] from pathlib import Path
OUT = ROOT / "docs" / "images"
SOURCES = OUT / "sources" ROOT = Path(__file__).resolve().parents[2]
BROWSERS = [ OUT = ROOT / "docs" / "images"
r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe", SOURCES = OUT / "sources"
r"C:\Program Files\Google\Chrome\Application\chrome.exe", BROWSERS = [
"microsoft-edge", r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
"google-chrome", r"C:\Program Files\Google\Chrome\Application\chrome.exe",
"chromium", "microsoft-edge",
] "google-chrome",
PORT = 5057 "chromium",
APP = "flask_day_54.presentation.hello" ]
PORT = 5057
TERMINAL_CSS = """ APP = "flask_day_54.presentation.hello"
body{margin:0;background:#1e1e1e;font-family:Consolas,'DejaVu Sans Mono',monospace;color:#d4d4d4}
.bar{background:#333;color:#ccc;padding:8px 14px;font-size:14px} TERMINAL_CSS = """
pre{margin:0;padding:16px 18px;font-size:15px;line-height:1.45;white-space:pre-wrap} body{margin:0;background:#1e1e1e;font-family:Consolas,'DejaVu Sans Mono',monospace;color:#d4d4d4}
.p{color:#6a9955}.c{color:#9cdcfe} .bar{background:#333;color:#ccc;padding:8px 14px;font-size:14px}
""" pre{margin:0;padding:16px 18px;font-size:15px;line-height:1.45;white-space:pre-wrap}
.p{color:#6a9955}.c{color:#9cdcfe}
BROWSER_CSS = """ """
body{margin:0;background:#f3f3f3;font-family:Segoe UI,Arial,sans-serif}
.tab{background:#dde3ea;padding:8px 14px;font-size:13px;color:#333} BROWSER_CSS = """
.url{background:#fff;margin:8px;padding:6px 12px;border:1px solid #bbb;border-radius:14px;font-size:14px} body{margin:0;background:#f3f3f3;font-family:Segoe UI,Arial,sans-serif}
.page{background:#fff;height:150px;padding:16px;font-size:16px;font-family:Times New Roman,serif} .tab{background:#dde3ea;padding:8px 14px;font-size:13px;color:#333}
""" .url{background:#fff;margin:8px;padding:6px 12px;border:1px solid #bbb;border-radius:14px;font-size:14px}
.page{background:#fff;height:150px;padding:16px;font-size:16px;font-family:Times New Roman,serif}
"""
def find_browser() -> str:
for candidate in BROWSERS:
found = shutil.which(candidate) or (candidate if os.path.exists(candidate) else None) def find_browser() -> str:
if found: for candidate in BROWSERS:
return found found = shutil.which(candidate) or (candidate if os.path.exists(candidate) else None)
raise SystemExit("No Edge or Chrome found for headless screenshots") if found:
return found
raise SystemExit("No Edge or Chrome found for headless screenshots")
def shot(page: Path, png: Path, width: int, height: int) -> None:
"""Take a headless screenshot of a local page or a URL."""
target = page.as_uri() if isinstance(page, Path) else page def shot(page: Path, png: Path, width: int, height: int) -> None:
profile = tempfile.mkdtemp(prefix="shot-") """Take a headless screenshot of a local page or a URL."""
subprocess.run( target = page.as_uri() if isinstance(page, Path) else page
[ profile = tempfile.mkdtemp(prefix="shot-")
find_browser(), subprocess.run( # nosemgrep
"--headless=new", [
"--disable-gpu", find_browser(),
"--hide-scrollbars", "--headless=new",
f"--user-data-dir={profile}", "--disable-gpu",
f"--window-size={width},{height}", "--hide-scrollbars",
f"--screenshot={png}", f"--user-data-dir={profile}",
"--virtual-time-budget=3000", f"--window-size={width},{height}",
target, f"--screenshot={png}",
], "--virtual-time-budget=3000",
check=True, target,
capture_output=True, ],
timeout=60, check=True,
) capture_output=True,
shutil.rmtree(profile, ignore_errors=True) timeout=60,
print("wrote", png.relative_to(ROOT)) )
shutil.rmtree(profile, ignore_errors=True)
print("wrote", png.relative_to(ROOT))
def run(cmd: list[str], cwd: Path, timeout: int = 30) -> str:
done = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout)
return (done.stdout + done.stderr).rstrip("\n") def run(cmd: list[str], cwd: Path, timeout: int = 30) -> str:
done = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout) # nosemgrep
return (done.stdout + done.stderr).rstrip("\n")
def terminal_page(title: str, entries: list[tuple[str, str]], png: Path, height: int) -> None:
"""Render (command, output) pairs as a terminal and take the screenshot."""
lines = [] def terminal_page(title: str, entries: list[tuple[str, str]], png: Path, height: int) -> None:
for command, output in entries: """Render (command, output) pairs as a terminal and take the screenshot."""
lines.append(f'<span class="p">$</span> <span class="c">{html.escape(command)}</span>') lines = []
if output: for command, output in entries:
lines.append(html.escape(output)) lines.append(f'<span class="p">$</span> <span class="c">{html.escape(command)}</span>')
page = ( if output:
f"<!doctype html><meta charset='utf-8'><style>{TERMINAL_CSS}</style>" lines.append(html.escape(output))
f"<div class='bar'>{html.escape(title)}</div><pre>" + "\n".join(lines) + "</pre>" page = (
) f"<!doctype html><meta charset='utf-8'><style>{TERMINAL_CSS}</style>"
SOURCES.mkdir(exist_ok=True) f"<div class='bar'>{html.escape(title)}</div><pre>" + "\n".join(lines) + "</pre>"
src = SOURCES / (png.stem + ".html") )
src.write_text(page, encoding="utf-8") SOURCES.mkdir(exist_ok=True)
shot(src, png, 900, height) src = SOURCES / (png.stem + ".html")
src.write_text(page, encoding="utf-8")
shot(src, png, 900, height)
def python() -> str:
return sys.executable
def python() -> str:
return sys.executable
def step1() -> None:
shot(SOURCES / "lecture-01.html", OUT / "lecture-01.png", 900, 400)
def step1() -> None:
shot(SOURCES / "lecture-01.html", OUT / "lecture-01.png", 900, 400)
def start_server() -> subprocess.Popen:
proc = subprocess.Popen(
[python(), "-m", "flask", "--app", APP, "run", "--port", str(PORT)], def start_server() -> subprocess.Popen:
cwd=ROOT, proc = subprocess.Popen( # nosemgrep
stdout=subprocess.PIPE, [python(), "-m", "flask", "--app", APP, "run", "--port", str(PORT)],
stderr=subprocess.STDOUT, cwd=ROOT,
text=True, stdout=subprocess.PIPE,
) stderr=subprocess.STDOUT,
for _ in range(40): text=True,
try: )
urllib.request.urlopen(f"http://127.0.0.1:{PORT}/", timeout=1).read() for _ in range(40):
return proc try:
except OSError: urllib.request.urlopen(f"http://127.0.0.1:{PORT}/", timeout=1).read()
time.sleep(0.25) return proc
proc.kill() except OSError:
raise SystemExit("server did not start") time.sleep(0.25) # nosemgrep
proc.kill()
raise SystemExit("server did not start")
def step2() -> None:
proc = start_server()
try: def step2() -> None:
body = urllib.request.urlopen(f"http://127.0.0.1:{PORT}/", timeout=3).read().decode() proc = start_server()
page = ( try:
f"<!doctype html><meta charset='utf-8'><style>{BROWSER_CSS}</style>" body = urllib.request.urlopen(f"http://127.0.0.1:{PORT}/", timeout=3).read().decode()
f"<div class='tab'>127.0.0.1:5057</div>" page = (
f"<div class='url'>http://127.0.0.1:{PORT}/</div>" f"<!doctype html><meta charset='utf-8'><style>{BROWSER_CSS}</style>"
f"<div class='page'>{body}</div>" f"<div class='tab'>127.0.0.1:5057</div>"
) f"<div class='url'>http://127.0.0.1:{PORT}/</div>"
src = SOURCES / "lecture-02.html" f"<div class='page'>{body}</div>"
src.write_text(page, encoding="utf-8") )
shot(src, OUT / "lecture-02.png", 700, 230) src = SOURCES / "lecture-02.html"
finally: src.write_text(page, encoding="utf-8")
proc.kill() shot(src, OUT / "lecture-02.png", 700, 230)
finally:
proc.kill()
def step3() -> None:
work = Path(tempfile.mkdtemp(prefix="cli-demo-"))
bash = shutil.which("bash") or "bash" def step3() -> None:
entries = [] work = Path(tempfile.mkdtemp(prefix="cli-demo-"))
script = [ bash = shutil.which("bash") or "bash"
"pwd", entries = []
"ls", script = [
"mkdir my_project", "pwd",
"cd my_project && touch hello.py && ls", "ls",
"cd .. && rm -rf my_project && ls", "mkdir my_project",
] "cd my_project && touch hello.py && ls",
for command in script: "cd .. && rm -rf my_project && ls",
output = run([bash, "-c", command], work) ]
entries.append((command, output.replace(str(work).replace("\\", "/"), "~/demo"))) for command in script:
shutil.rmtree(work, ignore_errors=True) output = run([bash, "-c", command], work)
terminal_page("Terminal - lecture 3: the command line", entries, OUT / "lecture-03.png", 330) entries.append((command, output.replace(str(work).replace("\\", "/"), "~/demo")))
shutil.rmtree(work, ignore_errors=True)
terminal_page("Terminal - lecture 3: the command line", entries, OUT / "lecture-03.png", 330)
def step4() -> None:
entries = []
cmd = "python -m flask_day_54.domain.name_demo" def step4() -> None:
entries.append((cmd, run([python(), "-m", "flask_day_54.domain.name_demo"], ROOT))) entries = []
imp = "python -c \"import flask_day_54.domain.name_demo as m; print(m.describe_name(m.__name__))\"" cmd = "python -m flask_day_54.domain.name_demo"
entries.append( entries.append((cmd, run([python(), "-m", "flask_day_54.domain.name_demo"], ROOT)))
( imp = "python -c \"import flask_day_54.domain.name_demo as m; print(m.describe_name(m.__name__))\""
imp, entries.append(
run( (
[ imp,
python(), run(
"-c", [
"import flask_day_54.domain.name_demo as m; print(m.describe_name(m.__name__))", python(),
], "-c",
ROOT, "import flask_day_54.domain.name_demo as m; print(m.describe_name(m.__name__))",
), ],
) ROOT,
) ),
server = subprocess.Popen( )
[python(), "-m", "flask_day_54.presentation.hello"], )
cwd=ROOT, server = subprocess.Popen( # nosemgrep
stdout=subprocess.PIPE, [python(), "-m", "flask_day_54.presentation.hello"],
stderr=subprocess.STDOUT, cwd=ROOT,
text=True, stdout=subprocess.PIPE,
) stderr=subprocess.STDOUT,
time.sleep(3) text=True,
server.kill() )
out = (server.stdout.read() if server.stdout else "").strip() time.sleep(3) # nosemgrep
entries.append(("python -m flask_day_54.presentation.hello", out + "\n^C")) server.kill()
terminal_page("Terminal - lecture 4: __name__ and __main__", entries, OUT / "lecture-04.png", 560) out = (server.stdout.read() if server.stdout else "").strip()
entries.append(("python -m flask_day_54.presentation.hello", out + "\n^C"))
terminal_page("Terminal - lecture 4: __name__ and __main__", entries, OUT / "lecture-04.png", 560)
def step5() -> None:
module = "flask_day_54.domain.first_class_functions"
entries = [(f"python -m {module}", run([python(), "-m", module], ROOT))] def step5() -> None:
terminal_page("Terminal - lecture 5: functions as first-class objects", entries, OUT / "lecture-05.png", 420) module = "flask_day_54.domain.first_class_functions"
entries = [(f"python -m {module}", run([python(), "-m", module], ROOT))]
terminal_page("Terminal - lecture 5: functions as first-class objects", entries, OUT / "lecture-05.png", 420)
def step6() -> None:
module = "flask_day_54.domain.decorators"
entries = [(f"python -m {module}", run([python(), "-m", module], ROOT))] def step6() -> None:
terminal_page("Terminal - lecture 6: decorators and the @ syntax", entries, OUT / "lecture-06.png", 380) module = "flask_day_54.domain.decorators"
entries = [(f"python -m {module}", run([python(), "-m", module], ROOT))]
terminal_page("Terminal - lecture 6: decorators and the @ syntax", entries, OUT / "lecture-06.png", 380)
STEPS = {1: step1, 2: step2, 3: step3, 4: step4, 5: step5, 6: step6}
if __name__ == "__main__": STEPS = {1: step1, 2: step2, 3: step3, 4: step4, 5: step5, 6: step6}
os.environ["PYTHONPATH"] = str(ROOT / "src")
for number in [int(a) for a in sys.argv[1:]] or STEPS: if __name__ == "__main__":
STEPS[number]() os.environ["PYTHONPATH"] = str(ROOT / "src")
for number in [int(a) for a in sys.argv[1:]] or STEPS:
STEPS[number]()