Week 1

Project Setup + First Endpoint

Install packages, scaffold the project, and serve your first GET /health response. Each cell is one runnable step.

1

Install packages

python
1!pip install fastapi uvicorn python-multipart pydantic \
2 rasterio pillow onnxruntime numpy nest_asyncio pyngrok

Plain-English explanation

In Google Colab this installs every Python library the app needs in one shell command.

Why it matters

Without these libraries, FastAPI cannot serve requests, Rasterio cannot read GeoTIFF, and ONNX Runtime cannot run your model.

Line by line

  • !pip installThe ! tells Colab to run a shell command instead of Python.
  • fastapi uvicornThe web framework and the ASGI server that actually accepts HTTP requests.
  • python-multipartLets FastAPI accept file uploads (multipart/form-data).
  • rasterio pillowRasterio reads GeoTIFF; Pillow handles JPG/PNG.
  • onnxruntime numpyLoads .onnx models and runs them on CPU.
  • nest_asyncio pyngrokLets Uvicorn run inside a Colab notebook and exposes a public URL.
Expected output
Successfully installed fastapi-0.115.0 uvicorn-0.30.6 rasterio-1.3.10 onnxruntime-1.18.1 ...

Common errors

ERROR: Could not find a version that satisfies the requirement rasterio
Restart the Colab runtime, then re-run. Pre-installed libs sometimes block the resolver.
Quick quiz

Why is python-multipart required?

2

Create the folder structure

python
1import os
2folders = ["app", "app/routers", "app/models", "app/utils", "uploads", "outputs"]
3for f in folders:
4 os.makedirs(f, exist_ok=True)
5print("Folders ready:", folders)

Plain-English explanation

Make sure every directory the app writes to or imports from exists before any other code runs.

Why it matters

FastAPI imports from app/, saves uploads to uploads/, and writes result PNGs to outputs/. A missing folder crashes the server on first request.

Line by line

  • import osStandard-library module for filesystem operations.
  • folders = [...]Plain list of every directory the project needs.
  • os.makedirs(f, exist_ok=True)Creates the folder; exist_ok=True means 'don't error if it already exists'.
Expected output
Folders ready: ['app', 'app/routers', 'app/models', 'app/utils', 'uploads', 'outputs']

Common errors

PermissionError: [Errno 13] Permission denied
You are trying to write outside /content in Colab. Use a relative path.
Quick quiz

What does exist_ok=True do?

3

Create app/main.py

python
1from fastapi import FastAPI
2from fastapi.middleware.cors import CORSMiddleware
3
4app = FastAPI(title="CEAMLS Building Damage Assessment API",
5 version="1.0.0")
6
7app.add_middleware(
8 CORSMiddleware,
9 allow_origins=["*"],
10 allow_methods=["*"],
11 allow_headers=["*"],
12)
13
14@app.get("/")
15def root():
16 return {"app": "CEAMLS Damage Assessment", "status": "running"}

Plain-English explanation

This is the application object. Every endpoint, router, and middleware attaches to this single app variable.

Why it matters

Without app = FastAPI(...) there is no server to register routes onto and Uvicorn cannot start anything.

Line by line

  • from fastapi import FastAPIBrings the framework class into scope.
  • app = FastAPI(title=...)Creates the application instance; the title appears in /docs (auto Swagger UI).
  • CORSMiddlewareAllows the browser frontend (served from a different origin) to call this API.
  • @app.get("/")Decorator: registers the function below as a handler for HTTP GET on /.
  • return {"app": ...}FastAPI auto-serializes the dict to JSON.
Expected output
# GET http://localhost:8000/ {"app": "CEAMLS Damage Assessment", "status": "running"}

Common errors

RuntimeError: Form data requires "python-multipart"
Install python-multipart and restart the server.
Quick quiz

What does the @app.get("/") decorator do?

4

Create app/schemas.py

python
1from pydantic import BaseModel
2from typing import List, Optional
3
4class DamageClass(BaseModel):
5 label: str
6 probability: float
7
8class ClassifyResponse(BaseModel):
9 filename: str
10 placard: str # GREEN | YELLOW | ORANGE | RED
11 top_class: str
12 confidence: float
13 entropy: float
14 classes: List[DamageClass]
15 recommendation: str
16
17class HealthResponse(BaseModel):
18 status: str
19 model_loaded: bool
20 version: str

Plain-English explanation

Pydantic models describe exactly what every request and response looks like. FastAPI uses them to validate input and generate the /docs page.

Why it matters

Schemas prevent the entire class of bugs where the frontend sends the wrong field name or wrong type — Pydantic rejects it with a clear 422 error before your handler runs.

Line by line

  • class DamageClass(BaseModel)One predicted class with a name and a probability between 0 and 1.
  • classes: List[DamageClass]A list of all four class predictions returned to the browser.
  • placard: strWill be 'GREEN', 'YELLOW', 'ORANGE', or 'RED'.
  • entropy: floatUncertainty score — higher means the model is less sure.
Expected output
# Pydantic validates incoming JSON automatically. {"status": "ok", "model_loaded": true, "version": "1.0.0"}

Common errors

pydantic.ValidationError: 1 validation error for ClassifyResponse
A field is missing or has the wrong type. Print your dict before returning it.
Quick quiz

Why use Pydantic schemas instead of plain dicts?

5

Create the health router

python
1# app/routers/health.py
2from fastapi import APIRouter
3from app.schemas import HealthResponse
4
5router = APIRouter(prefix="/health", tags=["health"])
6
7@router.get("", response_model=HealthResponse)
8def health():
9 return HealthResponse(status="ok", model_loaded=False, version="1.0.0")

Plain-English explanation

Routers are mini-FastAPI apps. The health router groups everything under /health into its own file.

Why it matters

Splitting routers per concern keeps main.py short and makes Week 3 (loading the ONNX model) a one-line change instead of a refactor.

Line by line

  • APIRouter(prefix="/health")Every route inside this file will live under /health.
  • response_model=HealthResponseFastAPI will coerce the return value into a HealthResponse before sending JSON.
  • model_loaded=FalseWe will flip this to True once Week 3 wires up ModelManager.
Expected output
# GET /health {"status": "ok", "model_loaded": false, "version": "1.0.0"}

Common errors

ModuleNotFoundError: No module named 'app'
Start Uvicorn from the project root (the folder that contains the app/ directory).
Quick quiz

What is the purpose of APIRouter?

6

Create the /classify stub

python
1# app/routers/classify.py
2from fastapi import APIRouter, UploadFile, File, HTTPException
3from app.schemas import ClassifyResponse, DamageClass
4
5router = APIRouter(prefix="/classify", tags=["classify"])
6
7@router.post("", response_model=ClassifyResponse)
8async def classify(file: UploadFile = File(...)):
9 if not file.filename:
10 raise HTTPException(400, "No file uploaded")
11
12 # TODO Week 2: validate, preprocess. Week 3: run model.
13 fake = [
14 DamageClass(label="no-damage", probability=0.72),
15 DamageClass(label="minor-damage", probability=0.18),
16 DamageClass(label="major-damage", probability=0.08),
17 DamageClass(label="destroyed", probability=0.02),
18 ]
19 return ClassifyResponse(
20 filename=file.filename, placard="GREEN", top_class="no-damage",
21 confidence=0.72, entropy=0.83, classes=fake,
22 recommendation="Safe to enter — routine inspection only.",
23 )

Plain-English explanation

A stub endpoint that accepts a file upload and returns hard-coded values. Lets the frontend integrate immediately while you build the model pipeline.

Why it matters

Frontend and backend work in parallel when the contract (the schema) is locked in early — even before the AI model exists.

Line by line

  • file: UploadFile = File(...)Declares an uploaded file parameter. The (...) means it is required.
  • raise HTTPException(400, "...")Returns HTTP 400 with a JSON error body.
  • return ClassifyResponse(...)FastAPI serializes the Pydantic model to JSON automatically.
Expected output
# POST /classify with any image file {"filename": "tile_001.tif", "placard": "GREEN", "top_class": "no-damage", "confidence": 0.72, "entropy": 0.83, "classes": [...], "recommendation": "Safe to enter — routine inspection only."}

Common errors

422 Unprocessable Entity
You forgot the 'file' form field. With curl use -F 'file=@image.tif'.
Quick quiz

Why build a stub before the real model?

7

Create the /compare router stub

python
1# app/routers/compare.py
2from fastapi import APIRouter
3router = APIRouter(prefix="/compare", tags=["compare"])
4
5@router.get("/models")
6def list_models():
7 return {"models": ["baseline", "ablation_a", "ablation_b",
8 "ablation_c", "ablation_d", "ablation_e"]}

Plain-English explanation

A placeholder endpoint that will eventually return a side-by-side comparison of all six ablation experiments on the same image.

Why it matters

The comparison endpoint is what graders use to see how augmentation, loss function, and backbone choices affect predictions on the same input.

Line by line

  • prefix="/compare"All routes here live under /compare.
  • @router.get("/models")Combined with the prefix this is GET /compare/models.
Expected output
{"models": ["baseline","ablation_a","ablation_b","ablation_c","ablation_d","ablation_e"]}
Quick quiz

What URL does this endpoint serve?

8

Create the /batch and /visualise routers

python
1# app/routers/batch.py
2from fastapi import APIRouter, UploadFile, File
3from typing import List
4router = APIRouter(prefix="/batch", tags=["batch"])
5
6@router.post("")
7async def batch_classify(files: List[UploadFile] = File(...)):
8 return {"received": [f.filename for f in files], "status": "stub"}
9
10# app/routers/visualise.py
11from fastapi import APIRouter
12router = APIRouter(prefix="/visualise", tags=["visualise"])
13
14@router.get("/probabilities")
15def probabilities():
16 return {"chart": "stub-png-url"}

Plain-English explanation

Two more stub routers so every URL in the final app exists from day one, returning placeholder data until the real implementations land.

Why it matters

Reserving URLs early prevents URL churn later — frontend code that calls /batch will not need to change when the real implementation ships.

Line by line

  • files: List[UploadFile]Accept many files in a single request — same form field name repeated.
  • [f.filename for f in files]List comprehension that returns just the filenames.
Expected output
# POST /batch with three .tif files {"received": ["tile_1.tif","tile_2.tif","tile_3.tif"], "status": "stub"}
Quick quiz

How does FastAPI know to accept multiple files?

9

Launch the server

python
1# Mount routers, then run uvicorn (Colab-friendly)
2from app.routers import health, classify, compare, batch, visualise
3
4app.include_router(health.router)
5app.include_router(classify.router)
6app.include_router(compare.router)
7app.include_router(batch.router)
8app.include_router(visualise.router)
9
10import nest_asyncio, uvicorn
11nest_asyncio.apply()
12uvicorn.run(app, host="0.0.0.0", port=8000)

Plain-English explanation

include_router() attaches each router to the main app. Then Uvicorn starts the actual HTTP server.

Why it matters

Until include_router() is called, FastAPI does not know your endpoints exist — visiting /health would return 404.

Line by line

  • app.include_router(health.router)Adds the /health endpoint to the app.
  • nest_asyncio.apply()Patches the Colab event loop so uvicorn.run() does not crash.
  • uvicorn.run(app, host="0.0.0.0", port=8000)Starts the ASGI server on every interface, port 8000.
Expected output
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) INFO: Application startup complete.

Common errors

RuntimeError: This event loop is already running
You forgot nest_asyncio.apply() before uvicorn.run() in Colab.
Quick quiz

What does uvicorn do?

10

Test every endpoint

python
1import requests
2BASE = "http://localhost:8000"
3
4print(requests.get(f"{BASE}/").json())
5print(requests.get(f"{BASE}/health").json())
6print(requests.get(f"{BASE}/compare/models").json())
7
8with open("uploads/sample.tif", "rb") as f:
9 r = requests.post(f"{BASE}/classify", files={"file": f})
10print(r.json())

Plain-English explanation

Smoke-tests every URL you just registered, including the file upload to /classify.

Why it matters

If any of these returns a 404 or 422 you know the issue is in your routing or schema — not the model.

Line by line

  • requests.get(f"{BASE}/health")Hits GET /health and parses the JSON response.
  • files={"file": f}requests sends the open file as the 'file' form field.
Expected output
{"app": "CEAMLS Damage Assessment", "status": "running"} {"status": "ok", "model_loaded": false, "version": "1.0.0"} {"models": ["baseline","ablation_a", ...]} {"filename":"sample.tif","placard":"GREEN", ...}

Common errors

ConnectionRefusedError
Uvicorn is not running. Re-run the launch cell.
Quick quiz

If GET /health returns 404, what is most likely wrong?