🔄 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

  1. How to turn an idea into a clear problem statement and user stories.

  2. How to define an MVP (minimum viable product) and acceptance criteria.

  3. A beginner‑friendly project structure for CLI or web/API apps.

  4. How to set up a project folder, virtual environment, and dependencies.

  5. 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 pip

Note: 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.txt

Track 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 1

Expected 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 = False

app/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 --reload

Open: 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 -i

Expected 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 Content

Flask 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

  1. Choose Track A (CLI) or Track B (API).

  2. Create the folder layout and set up your virtual environment.

  3. Add dependencies and save requirements.txt.

  4. Add the skeleton files and run the app.

  5. Update README.md with: 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.

Keep Reading