🔄 Quick Recap (Day 27)
You automated scripts and scheduled them on macOS/Linux (cron) or Windows (Task Scheduler), and you safely ran system commands with
subprocess.
🎯 What You’ll Learn Today
How to turn an idea into a clear problem statement and user stories.
How to define an MVP (minimum viable product) and acceptance criteria.
A beginner‑friendly project structure for CLI or web/API apps.
How to set up a project folder, virtual environment, and dependencies.
How to create a running skeleton (CLI or FastAPI/Flask) you’ll build out tomorrow.
🧭 Pick Your Track (choose one)
A) CLI: Habit Tracker (console app)
Log daily habits (e.g., reading, exercise). View, add, mark done. Store data in a JSON file.
B) Web/API: Pocket Tasks (tiny REST API)
Create tasks via HTTP. List tasks, mark done, delete. In‑memory today; persistence later.
Tip: If you enjoy terminals, pick CLI. If you prefer browsers and APIs, pick Web/API.
✍️ Problem Statement & User Stories
Problem statement:
I want a lightweight tool to record and review my daily tasks/habits so I stay consistent.
User stories (start with these):
As a user, I can add a task/habit with a short title.
As a user, I can list items so I can see what’s pending.
As a user, I can mark an item done.
As a user, I can delete an item I no longer need.
MVP acceptance criteria:
Add/list/mark/delete all work end‑to‑end.
Clear feedback on success or helpful error messages on failure.
Data stored (CLI → JSON file; API → in memory for now).
🧱 Suggested Folder Structure
mini_project/
├─ app/
│ ├─ __init__.py
│ ├─ models.py # dataclasses / pydantic models
│ ├─ storage.py # file or in‑memory store
│ └─ main.py # CLI or web entry point
├─ tests/ # optional today; expand tomorrow
├─ data/ # for CLI JSON storage (created at runtime)
├─ README.md
├─ requirements.txt⚙️ Setup: Repo, Env, Deps
From your working folder:
mkdir mini_project && cd mini_project
python -m venv env
# Activate your env (Windows: env\Scripts\activate) (macOS/Linux: source env/bin/activate)
pip install --upgrade pipNote: Git/GitHub are optional right now. If you already use them, you can initialize a repo and add a .gitignore. If not, skip it.
Track A (CLI) – dependencies
pip install rich
pip freeze > requirements.txtTrack B (Web/API) – dependencies
Choose FastAPI (recommended) or Flask.
# FastAPI
pip install fastapi uvicorn
# or Flask
pip install flask
pip freeze > requirements.txt🧩 Track A: CLI Skeleton (Habit Tracker)
app/models.py
from dataclasses import dataclass
from datetime import datetime
@dataclass
class Habit:
id: int
title: str
done: bool = False
created_at: str = datetime.now().isoformat(timespec="seconds")app/storage.py
from __future__ import annotations
from pathlib import Path
import json
from typing import List, Dict
DATA_PATH = Path(__file__).resolve().parent.parent / "data" / "habits.json"
DATA_PATH.parent.mkdir(parents=True, exist_ok=True)
# very simple JSON persistence
def load() -> List[Dict]:
if not DATA_PATH.exists():
return []
return json.loads(DATA_PATH.read_text(encoding="utf-8"))
def save(items: List[Dict]) -> None:
DATA_PATH.write_text(json.dumps(items, indent=2), encoding="utf-8")app/main.py
from __future__ import annotations
import argparse
from typing import List, Dict
from app import storage
def add(title: str) -> None:
items: List[Dict] = storage.load()
new_id = (max([i["id"] for i in items]) + 1) if items else 1
items.append({"id": new_id, "title": title, "done": False})
storage.save(items)
print(f"Added #{new_id}: {title}")
def ls() -> None:
items = storage.load()
if not items:
print("No habits yet. Try: add <title>")
return
for i in items:
mark = "✔" if i["done"] else "✗"
print(f"[{mark}] {i['id']}: {i['title']}")
def done(item_id: int) -> None:
items = storage.load()
for i in items:
if i["id"] == item_id:
i["done"] = True
storage.save(items)
print(f"Marked #{item_id} as done")
return
print(f"No item with id {item_id}")
def delete(item_id: int) -> None:
items = storage.load()
new_items = [i for i in items if i["id"] != item_id]
storage.save(new_items)
print(f"Deleted #{item_id}" if len(new_items) != len(items) else f"No item with id {item_id}")
def main() -> None:
p = argparse.ArgumentParser(prog="habits", description="Habit Tracker")
sub = p.add_subparsers(dest="cmd", required=True)
p_add = sub.add_parser("add", help="Add a new habit")
p_add.add_argument("title")
sub.add_parser("list", help="List habits")
p_done = sub.add_parser("done", help="Mark as done")
p_done.add_argument("id", type=int)
p_del = sub.add_parser("del", help="Delete an item")
p_del.add_argument("id", type=int)
args = p.parse_args()
if args.cmd == "add":
add(args.title)
elif args.cmd == "list":
ls()
elif args.cmd == "done":
done(args.id)
elif args.cmd == "del":
delete(args.id)
if __name__ == "__main__":
main()Run (examples):
python -m app.main add "Read 10 pages"
python -m app.main list
python -m app.main done 1
python -m app.main del 1Expected output:
Added #1: Read 10 pages
[✗] 1: Read 10 pages
Marked #1 as done
Deleted #1🌐 Track B: Web/API Skeleton (Pocket Tasks)
FastAPI version shown; Flask snippet included after.
app/models.py
from pydantic import BaseModel
class Task(BaseModel):
id: int
title: str
done: bool = Falseapp/main.py (FastAPI)
from fastapi import FastAPI, HTTPException
from app.models import Task
app = FastAPI(title="Pocket Tasks")
DB: list[Task] = []
@app.get("/")
async def root():
return {"message": "Pocket Tasks API"}
@app.get("/tasks")
async def list_tasks() -> list[Task]:
return DB
@app.post("/tasks", status_code=201)
async def add_task(task: Task) -> Task:
if any(t.id == task.id for t in DB):
raise HTTPException(status_code=400, detail="id already exists")
DB.append(task)
return task
@app.patch("/tasks/{task_id}")
async def mark_done(task_id: int) -> Task:
for t in DB:
if t.id == task_id:
t.done = True
return t
raise HTTPException(status_code=404, detail="not found")
@app.delete("/tasks/{task_id}", status_code=204)
async def delete_task(task_id: int) -> None:
global DB
before = len(DB)
DB = [t for t in DB if t.id != task_id]
if len(DB) == before:
raise HTTPException(status_code=404, detail="not found")Run (FastAPI):
uvicorn app.main:app --reloadOpen: http://127.0.0.1:8000/
Docs: http://127.0.0.1:8000/docs
Try with curl (or your browser):
curl -X POST http://127.0.0.1:8000/tasks -H 'Content-Type: application/json' \
-d '{"id":1, "title":"Study 20 minutes"}'
curl http://127.0.0.1:8000/tasks
curl -X PATCH http://127.0.0.1:8000/tasks/1
curl -X DELETE http://127.0.0.1:8000/tasks/1 -iExpected responses (abridged):
{"id":1,"title":"Study 20 minutes","done":false}
[{"id":1,"title":"Study 20 minutes","done":false}]
{"id":1,"title":"Study 20 minutes","done":true}
HTTP/1.1 204 No ContentFlask alternative (optional)
# app/main.py
from flask import Flask, jsonify, request, abort
app = Flask(__name__)
DB = []
@app.get("/")
def root():
return jsonify({"message": "Pocket Tasks API (Flask)"})
@app.get("/tasks")
def list_tasks():
return jsonify(DB)
@app.post("/tasks")
def add_task():
task = request.get_json(force=True)
if any(t.get("id") == task.get("id") for t in DB):
abort(400, "id already exists")
task.setdefault("done", False)
DB.append(task)
return jsonify(task), 201
@app.patch("/tasks/<int:task_id>")
def mark_done(task_id):
for t in DB:
if t.get("id") == task_id:
t["done"] = True
return jsonify(t)
abort(404)
@app.delete("/tasks/<int:task_id>")
def delete_task(task_id):
global DB
before = len(DB)
DB = [t for t in DB if t.get("id") != task_id]
if len(DB) == before:
abort(404)
return ("", 204)
if __name__ == "__main__":
app.run(debug=True)Run with:
python -m app.main✅ Definition of Done (for today)
Project folder includes a clear README.md (problem statement, user stories, run instructions).
Virtual environment active; dependencies pinned in requirements.txt.
Project structure created under
mini_project/.CLI or API skeleton runs and responds as shown above.
🧙♂️ Take the Wand and Try Yourself
Choose Track A (CLI) or Track B (API).
Create the folder layout and set up your virtual environment.
Add dependencies and save
requirements.txt.Add the skeleton files and run the app.
Update
README.mdwith: problem statement, user stories, run commands, and screenshots (optional).
bash git add . git commit -m "Day 28: project skeleton running"
Expected outcome:
CLI prints items and updates the JSON file; API returns the sample JSON and status codes.
Your README explains how to run the project in under 1 minute.
Up next: Day 29: Mini Project Build — add real features, tests, and persistence.