Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# TaskBox local configuration (safe defaults for the SQLite lab)
APP_ENV=development
# `development` permits the placeholder secrets below for disposable local labs only.
TASKBOX_ENV=development
TASKBOX_DATABASE_URL=sqlite:///./taskbox.db
TASKBOX_JWT_SECRET=change-me-in-development
TASKBOX_JWT_EXPIRES=3600
Expand Down
8 changes: 8 additions & 0 deletions .postman/resources.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Use this workspace to collaborate
workspace:
id: 7e09ac8a-2f0a-42c4-9f4e-81503aaca133

localResources:
specs:
- ../contracts/taskbox.openapi.json
- ../course/labs/01-http-api-design/solution/openapi.yaml
12 changes: 9 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@ FROM python:3.13-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy
UV_LINK_MODE=copy \
TASKBOX_ENV=production \
TASKBOX_DATABASE_URL=sqlite:////data/taskbox.db
WORKDIR /app
RUN pip install --no-cache-dir uv
RUN groupadd --system taskbox
RUN useradd --system --gid taskbox --no-create-home --home-dir /nonexistent --shell /usr/sbin/nologin taskbox
RUN install --directory --owner=taskbox --group=taskbox /data
COPY pyproject.toml uv.lock README.md LICENSE ./
COPY src ./src
COPY migrations ./migrations
COPY alembic.ini ./
RUN uv sync --frozen --no-dev
EXPOSE 8000
USER taskbox
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD ["/app/.venv/bin/python", "-c", "from urllib.request import urlopen; urlopen('http://127.0.0.1:8000/healthz', timeout=3).read()"]
CMD ["/app/.venv/bin/uvicorn", "taskbox.main:app", "--host", "0.0.0.0", "--port", "8000"]
24 changes: 16 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@

This repository is a beginner-to-production course for designing, building, testing, and operating APIs.

The new course uses Python, FastAPI, SQLite, and a TaskBox capstone. It includes runnable labs, generated OpenAPI contracts, authentication, webhooks, observability, and deployment exercises. Historical material is preserved under [`legacy/`](legacy/).
The new course uses Python, FastAPI, SQLite, and a TaskBox capstone. It includes runnable labs, generated OpenAPI contracts, authentication, webhooks, observability, and deployment exercises.

## Start here

1. Install Python 3.13+, Node 24 LTS, and `uv`.
1. Install Postman, Python 3.13+, Node 24 LTS, and `uv`.
2. Run `uv sync --all-groups --frozen`.
3. Start TaskBox with `uv run uvicorn taskbox.main:app --reload`.
4. Open the API docs at `http://127.0.0.1:8000/docs`.
5. Follow the 40-hour sequence in [`course/course-map.yml`](course/course-map.yml).
6. Start the course site with `cd site && npm ci && npm run dev`.
5. Complete required [Prerequisite Lab 00: Postman foundations](course/labs/00-postman-prerequisite/README.md).
6. Follow the 43-hour sequence in [`course/course-map.yml`](course/course-map.yml), beginning Lab 01 only after the prerequisite.
7. Start the course site with `cd site && npm ci && npm run dev`.

Local site routes start at `http://localhost:4321/`. The GitHub Pages build uses
`/API/`, so the deployed setup page is `https://ialimustufa.github.io/API/setup/`.
Expand All @@ -35,16 +36,23 @@ The root Compose stack deploys the SQLite-first TaskBox API with a persistent
Docker volume:

```bash
export TASKBOX_JWT_SECRET="$(openssl rand -hex 32)"
export TASKBOX_WEBHOOK_SECRET="$(openssl rand -hex 32)"
docker compose up --build
curl http://127.0.0.1:8000/healthz
curl http://127.0.0.1:8000/readyz
```

Set `TASKBOX_JWT_SECRET` and `TASKBOX_WEBHOOK_SECRET` to long random values in
`.env` before exposing the API. The PostgreSQL transition is a separate required
exercise in [`course/labs/07-operations`](course/labs/07-operations/).

Compose refuses to start without both secrets, and the production image rejects
the course placeholder values. Store long random values in `.env` instead of
exporting them when that better fits your local workflow. The PostgreSQL
transition is a separate required exercise in
[`course/labs/07-operations`](course/labs/07-operations/).

## License

Authored course and application code is MIT licensed. Historical third-party material retains its original provenance; see [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md).

## Legacy

The original course materials are preserved under [`legacy/`](legacy/).
29 changes: 0 additions & 29 deletions alembic.ini

This file was deleted.

5 changes: 3 additions & 2 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ services:
app:
build: .
environment:
TASKBOX_ENV: production
TASKBOX_DATABASE_URL: sqlite:////data/taskbox.db
TASKBOX_JWT_SECRET: ${TASKBOX_JWT_SECRET:-change-me-in-development}
TASKBOX_WEBHOOK_SECRET: ${TASKBOX_WEBHOOK_SECRET:-change-me-in-development}
TASKBOX_JWT_SECRET: "${TASKBOX_JWT_SECRET:?Set TASKBOX_JWT_SECRET to a non-default secret}"
TASKBOX_WEBHOOK_SECRET: "${TASKBOX_WEBHOOK_SECRET:?Set TASKBOX_WEBHOOK_SECRET to a non-default secret}"
ports:
- "8000:8000"
volumes:
Expand Down
7 changes: 4 additions & 3 deletions contracts/taskbox.openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
},
"servers": [{"url": "http://localhost:8000", "description": "Local development"}],
"tags": [
{"name": "Health"},
{"name": "Auth"},
{"name": "Projects"},
{"name": "Tasks"},
{"name": "Webhooks"}
],
"paths": {
"/healthz": {
"get": {"operationId": "healthCheck", "tags": ["Auth"], "responses": {"200": {"description": "Service is healthy", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Health"}}}}}}
"get": {"operationId": "healthCheck", "tags": ["Health"], "responses": {"200": {"description": "Service is healthy", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Health"}}}}}}
},
"/api/v1/auth/register": {
"post": {
Expand Down Expand Up @@ -200,8 +201,8 @@
"Task": {"type": "object", "required": ["id", "project_id", "created_by", "title", "status", "priority", "created_at", "updated_at"], "properties": {"id": {"type": "string", "format": "uuid"}, "project_id": {"type": "string", "format": "uuid"}, "created_by": {"type": "string", "format": "uuid"}, "assignee_id": {"type": ["string", "null"], "format": "uuid"}, "title": {"type": "string", "maxLength": 240}, "description": {"type": ["string", "null"], "maxLength": 10000}, "status": {"$ref": "#/components/schemas/TaskStatus"}, "priority": {"type": "integer", "minimum": 0, "maximum": 4}, "due_at": {"type": ["string", "null"], "format": "date-time"}, "created_at": {"type": "string", "format": "date-time"}, "updated_at": {"type": "string", "format": "date-time"}}},
"TaskCreate": {"type": "object", "required": ["title"], "properties": {"title": {"type": "string", "minLength": 1, "maxLength": 240}, "description": {"type": ["string", "null"], "maxLength": 10000}, "status": {"$ref": "#/components/schemas/TaskStatus"}, "priority": {"type": "integer", "minimum": 0, "maximum": 4}, "assignee_id": {"type": ["string", "null"], "format": "uuid"}, "due_at": {"type": ["string", "null"], "format": "date-time"}}},
"TaskUpdate": {"type": "object", "minProperties": 1, "properties": {"title": {"type": "string", "minLength": 1, "maxLength": 240}, "description": {"type": ["string", "null"], "maxLength": 10000}, "status": {"$ref": "#/components/schemas/TaskStatus"}, "priority": {"type": "integer", "minimum": 0, "maximum": 4}, "assignee_id": {"type": ["string", "null"], "format": "uuid"}, "due_at": {"type": ["string", "null"], "format": "date-time"}}},
"WebhookImportRequest": {"type": "object", "required": ["project_id", "tasks"], "properties": {"project_id": {"type": "string", "format": "uuid"}, "tasks": {"type": "array", "minItems": 1, "items": {"$ref": "#/components/schemas/TaskCreate"}}}},
"WebhookImportResponse": {"type": "object", "required": ["event_id", "imported", "duplicate"], "properties": {"event_id": {"type": "string"}, "imported": {"type": "integer", "minimum": 0}, "duplicate": {"type": "boolean"}}},
"WebhookImportRequest": {"type": "object", "required": ["project_id", "tasks"], "properties": {"project_id": {"type": "string", "format": "uuid"}, "actor_id": {"type": ["string", "null"], "format": "uuid", "description": "Optional actor when no Bearer token is supplied; see the threat-model guidance."}, "tasks": {"type": "array", "minItems": 1, "items": {"$ref": "#/components/schemas/TaskCreate"}}}},
"WebhookImportResponse": {"type": "object", "required": ["event_id", "imported"], "properties": {"event_id": {"type": "string"}, "imported": {"type": "integer", "minimum": 0}}},
"PageInfo": {"type": "object", "required": ["next_cursor"], "properties": {"next_cursor": {"type": ["string", "null"]}}},
"ProjectPage": {"type": "object", "required": ["items", "next_cursor"], "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/Project"}}, "next_cursor": {"type": ["string", "null"]}}},
"MembershipPage": {"type": "object", "required": ["items", "next_cursor"], "properties": {"items": {"type": "array", "items": {"$ref": "#/components/schemas/Membership"}}, "next_cursor": {"type": ["string", "null"]}}},
Expand Down
13 changes: 11 additions & 2 deletions course/course-map.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
title: API Engineering with TaskBox
version: 1
baseline: python-3.13
duration_hours: 40
duration_hours: 43
format: self-paced
modules:
- id: postman-prerequisite
title: Postman foundations with TaskBox
hours: 3
prerequisite: true
labs: [00-postman-prerequisite]
outcomes:
- Use the local TaskBox collection, variables, authorization, scripts, examples, and documentation safely
- Run and troubleshoot the dependency-ordered TaskBox workflow before beginning Lab 01
- id: http-api-design
title: HTTP and API design
hours: 4
Expand Down Expand Up @@ -82,8 +90,9 @@ modules:
- Integrate the course capabilities into one production-minded API
- Document migration, rollback, threat-model, and smoke-test plans
course_completion:
required_hours: 40
required_hours: 43
required_labs:
- 00-postman-prerequisite
- 01-http-api-design
- 02-fastapi-basics
- 03-crud
Expand Down
2 changes: 2 additions & 0 deletions course/examples/webhooks/sign.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Signer for the timestamped Lab 10 receiver, not the TaskBox reference API."""

import hashlib
import hmac
import json
Expand Down
27 changes: 27 additions & 0 deletions course/examples/webhooks/sign_taskbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Create headers and a body for TaskBox's raw-body HMAC webhook endpoint.

Set TASKBOX_PROJECT_ID and TASKBOX_ACTOR_ID from a TaskBox project before
sending the printed body to POST /api/v1/webhooks/tasks/import.
"""

import hashlib
import hmac
import json
import os
import uuid

secret = os.getenv("TASKBOX_WEBHOOK_SECRET", "dev-webhook-secret").encode()
event_id = os.getenv("TASKBOX_WEBHOOK_EVENT_ID", f"evt-{uuid.uuid4()}")
body = json.dumps(
{
"project_id": os.environ["TASKBOX_PROJECT_ID"],
"actor_id": os.environ["TASKBOX_ACTOR_ID"],
"tasks": [{"title": "Imported vendor task", "priority": 2}],
},
separators=(",", ":"),
).encode()
signature = hmac.new(secret, body, hashlib.sha256).hexdigest()

print(f"X-Webhook-Event-ID: {event_id}")
print(f"X-Webhook-Signature: sha256={signature}")
print(body.decode())
Loading
Loading