MIL-002: backend basics and Hello Flask #33
+214
-212
@@ -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]()
|
||||||
Reference in new issue
Block a user