From f253856b78ca377e174b8b97bcd07a54982328af Mon Sep 17 00:00:00 2001 From: vanamaeshwar7-web Date: Tue, 10 Feb 2026 02:37:00 +0530 Subject: [PATCH] Add files via upload --- README.md | 25 ++++ app/__init__.py | 0 app/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 173 bytes app/__pycache__/crud.cpython-311.pyc | Bin 0 -> 2885 bytes app/__pycache__/db.cpython-311.pyc | Bin 0 -> 1137 bytes app/__pycache__/main.cpython-311.pyc | Bin 0 -> 3478 bytes app/__pycache__/models.cpython-311.pyc | Bin 0 -> 1605 bytes app/__pycache__/schemas.cpython-311.pyc | Bin 0 -> 2258 bytes app/crud.py | 56 +++++++++ app/db.py | 28 +++++ app/main.py | 57 +++++++++ app/models.py | 21 ++++ app/schemas.py | 32 +++++ db.sqlite3 | Bin 0 -> 12288 bytes .../test_tasks.cpython-311-pytest-9.0.2.pyc | Bin 0 -> 16093 bytes tests/test_tasks.py | 110 ++++++++++++++++++ 16 files changed, 329 insertions(+) create mode 100644 app/__init__.py create mode 100644 app/__pycache__/__init__.cpython-311.pyc create mode 100644 app/__pycache__/crud.cpython-311.pyc create mode 100644 app/__pycache__/db.cpython-311.pyc create mode 100644 app/__pycache__/main.cpython-311.pyc create mode 100644 app/__pycache__/models.cpython-311.pyc create mode 100644 app/__pycache__/schemas.cpython-311.pyc create mode 100644 app/crud.py create mode 100644 app/db.py create mode 100644 app/main.py create mode 100644 app/models.py create mode 100644 app/schemas.py create mode 100644 db.sqlite3 create mode 100644 tests/__pycache__/test_tasks.cpython-311-pytest-9.0.2.pyc create mode 100644 tests/test_tasks.py diff --git a/README.md b/README.md index 494f1c75..74dd5f84 100644 --- a/README.md +++ b/README.md @@ -41,3 +41,28 @@ Your solution should include at least one real workflow, for example: - When you are complete, put up a Pull Request against this repository with your changes. - A short summary of your approach and tools used in your PR submission - Any additional information or approach that helped you. + +# Task Manager API + +A simple backend application built using a spec-driven development approach. +The API supports creating, listing (with filtering and search), updating, and deleting tasks, with data persisted locally using SQLite. + +The project demonstrates: + +Clear API specification (SPEC.md) + +Clean FastAPI + SQLAlchemy architecture + +Automated test coverage with pytest + +Local development and execution +## Tech Stack +- Python 3.11 +- FastAPI +- SQLite +- SQLAlchemy +- Pytest + +## Run the application +```bash +python -m uvicorn app.main:app --reload diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/__pycache__/__init__.cpython-311.pyc b/app/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..47d5aeb55fa96a67fdd8a1c2acb5063e48c08db3 GIT binary patch literal 173 zcmZ3^%ge<81U#l)nIQTxh=2h`DC095kTIPhg&~+hlhJP_LlF~@{~09t%fZMD6=dzPd6pCEHx*;AU8FyL^n4vGcP8wpdcnbJ~J<~ rBtBlRpz;@oO>TZlX-=wL5i8I@kd?*!K;i>4BO~Jn1{hJq3={(Z6G|#A literal 0 HcmV?d00001 diff --git a/app/__pycache__/crud.cpython-311.pyc b/app/__pycache__/crud.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..010bdd43454d827c68cc090d3a1d9101e14ea2a9 GIT binary patch literal 2885 zcmbUjO>YxNbY?%ij&~iKU?3?}`53x`7p;O|dY)4K{n|8S-O!8)<~6A*V0K?Iv9gJ&@2v}8&dc}50WCW@(K)EO0M zm0}X00iwYZyrLm=AwW7-lo?G(LNs_mAv*9KWRI9J8hRw$pkPJkiaHZ&tRSERa}ijj zH|D@f6nKLKr%}A?DL-f&JK8k0taNq}$bdynnzEl_SE8ItS__Uu&O54|woU5jgjy+< z&e@+v9b|J0b;#qqMDXv@I>oC9R$uI&< z>81Q7#rN9uSfS_H_ojh(g+&=WAWu1poh8}0%y`gP2Ow~PowS#%xWWRQm6L=xfmAk= zN!yOb=mkctc@RR3P}3^K22WX#3aAg@#pu|{*>5b$tl9b8Y=T+~b~ZO_aT9e1?z-@Xn)93-7ic60C z9kqo=ZK*hmVL>{BPY&Xg(UJPWAj=4X$A?2+2OqW>Qrl9chO6-JYEoQ+Ga^&-x5gs zpP~5g%U6{*vdXeZJP~2eLZ*H-w-_KJ=Ebe;BGmcaD}LBbCTV zQF)>nHKeMcs?ois7AM6I*Oea|(VOEt#-VNF(A}@gMsLOFg{g{hXvY}XHU>(^%EnN| z7IU0J702LMJ1V;4Msnt{WWA}P@dWN@qhRZ!iD?LZc`bb3|DI|DNom@S+ zM#}oZihgiM@7>mW%X)uB@844TMS1y3iaS}9)UQCiT`S-zW=RB1qr8-t=e!mHZN)1n zFz$gr;G(lnQLnp-q4`3v>+_h@TGh$L>&)91jMo}ZlnfsY8AGVW@)T6t4p!pp+JJk(wR!S@7L zb=&@jAo&;`YYfnTId)$)5-Tp%@3tbjc6Q}#;jI6v(<8;>!ky41rs83z)4F6P&D1>2 zEFa0T4CAM8LN6yp6LA8WEF8$H-~5)%#O>?I=|!9IOGy;DNC3tU`vAGu5Wl`&1V>@< zl1aZ{$AIM@FV;T5H5p_41PyHY|5fzvmj7Qx-CO>DEzp6ZYhTt745cwK{E>Q?`17J5 zTTwU0{c8sx?{I(V6EU=+U?(1^AwX-NiJ>&MG5E{4ARne4CH?}t4UHyhK*Q0yumFaQ aX)!!JEQVGT3%I9+p_Hm27+#Bn+x`U-Bq=EX literal 0 HcmV?d00001 diff --git a/app/__pycache__/db.cpython-311.pyc b/app/__pycache__/db.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e2301a12bf06b0099a6778044de91a8c0c5c429f GIT binary patch literal 1137 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 literal 0 HcmV?d00001 diff --git a/app/__pycache__/models.cpython-311.pyc b/app/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c8482587e12e23ac934a5455b66e6a81f18d30f GIT binary patch literal 1605 zcma)6J#5=X6h8ilqA1Ijf1p-Q0~D~UQ~{J9Bxr#)4WP(%g$79n*$W|rV04#;HUDy? zT)SB`bPx&;0y;zmI=DdLCa?z&9Xxi-NC6rIP6o2nn}cM^)OTd$RqX!U%IFvXF{6Ci5m|@hWc#s$hw#7_A9r z!je?UN~%fFMVv5YE2XA5#G|hemVQJyNm4rkLc1{Ip(FjfvNwhU*)SeKZH#LeY1y16WB+9_UTer!#C- z@_gNIdPR=D$5f?33A7Q`bic^QOR{4(UDkD>62BwkTqtXrty@IXLPgUo2RBV7PiWes zrf$BqWHilZX~im)Aq34b1_eZ+VAy^*rD?itJCFrr>S-DknSD00dp@PCH7Pp)eV;Lj zG4^$YQqynP&i9m+f}R8R47~!5-rTHwR$ui9_397Z`V#The8;VOE@_l7HP(q;!epJ8 zj%yLyFIl=_*Wn|n!wHD#U2-?+1OsG@rR!aU{%!r|8O)t)-8sxHbgX^xU@6Fzd%1Ey zS8m;EOKoo}9f)P1o>(5_3*GbE>)Y#38^1mN{qezSkgxRem43d``m+5=XJPB}K&$}u z#LA#>ZQI&k`s;d7sP+ogexcgB-N|)@F8)R7PcH;w6{sgx2lE%YrKb`+^qI1!94tRy z4d!dT`C5Oz)>>{acdl*S4#XNzPplm-T-p9&AZNQD|9bgY;1{Kr2-s`JT3>gh&PG4` z;W6S9QpCUrl7tqoo;dAS|NQ1%9ZzuRN5jjzTTI7wQoKi|ZZ;kgYcsBGR8dyPaPnW% zC5Kv3S42$`^7uYwf8Mwli%g1;3n0CbZ^Z3(lU@Xs{a?JFfgKAR#~q^c!6*$-DfoXH fpew;yG(dB~C>=`~4le67YhQoKR$rfCijw{ZSTT&5 literal 0 HcmV?d00001 diff --git a/app/__pycache__/schemas.cpython-311.pyc b/app/__pycache__/schemas.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..47b4fe562235b2fc59609a1f86adffb2bb771ebb GIT binary patch literal 2258 zcma)6&2JM&6rbIlU4MooErx`TIB5u})rP7ih_*tlR4|d!HXK4Ot4OP@XOb-Jhr7E* zq~c&C4pG%Z4yg3ViME16|AZcUTq8?ZdqV1on~^!?)c0m%JGOJ^c=qQv@6F7c_nY_T z_pz}ofp+tYpRJD-LjJ`;x2cCh=T{J(5Js3}6Nl(TLRq#YN7iLW(G@XPZ0e}G>S($q z5w0;h-Q9=iDVeMgramG}1kn0fh|2^%LrQrY}pZ8mgUw-DgOqyDM@jf znIR7?hnM6y^Nkts?>)xtSiNa+n}IHUs~TFKYua(@J1gYAX_u6kh92|coaGu--`n!hmt|9@~U?}FOOnZ?U znv>#buJ$Q{`DvWbAXq)aR0u5_XK$Oq-RnMw$lgd`eDe1GD_Bs*>*(wQJRuJV#+kII=yjw$1I_cE z_5m@D$ai`>Kb(b~dZYijQ*S;a>yTu?g0xmr{7LW-t3e3I_umH@IG_Qyx8jpVpCZGk zcy7q=hXL*~JA3f;^)EL!0`3Q!JJrn_Jh&Tr)y<&FD@)9`?s0dC@q66%st$L8kTE?Kb`4&yEJ4Q1D`<|piWwa>Bsu3!othKLZh(Q zEG+KPU$U*-cr$l4$}P5v^UdNXuZmY*7OymlSDVGFdpDZ-bCKw6h2%ekNgdb-Gazjk zuIX^Yh_i+P9&0wrdBgazX4;9x$1+VAQh=d&+%Qbn^+FTVJunPGn6Kd^Ot(LYe+K0# zgnSn=AOzb0^&`L5NM|2r_gD$t`E

%5&li1xpHH! z+?*@dzulkuZT|83h?b!@Xt_-l_476vh2#wAexTI4uR;fpc(l~G!YPwPfyj^)N<4u# zu~EgMUx!d@L97Mhf32kYXJNH}4&gjNQbx%vzlhpdggy%&;1B^*E0_ToU?G1hqSzY( zK}eS3f`P+RP+;r$0LLF5-UZ5J41;R})lZosv?;KQcLQDr%2c64NkggsP!7E%8I}?6 zMG8r|RSRR;Vj?4wyPt=(ake7f5oVZSoUIAsP#%b&TkW0q1|ndRt}sFS!HGi;m#%y6 zmbHC*twe{}9^djk2X=;H{h=1}V2JHlOP2Z{fe*Zm09o%7-a0e0H;%uA0EuKzVdLTl{-;it*<^@ZB0%m zsXP|!$DQ~hSW&(rdqs4!Xy~Q&LHb(qV0`6&3I@E}AO>hFk|ez*=cB>i zA`8)Ry+tlW$Mx4^C!@k=&9Te%Y+Fl7Q01erKm8W9@A{^bl5`pf8iD;?ph!9m6m?DM Tj09AU!k+aOweOBGiP--G*%bMs literal 0 HcmV?d00001 diff --git a/app/crud.py b/app/crud.py new file mode 100644 index 00000000..831bc8f0 --- /dev/null +++ b/app/crud.py @@ -0,0 +1,56 @@ +# CRUD operations +from sqlalchemy.orm import Session +from sqlalchemy import select +from .models import Task + + +def create_task(db: Session, title: str, description: str | None): + task = Task( + title=title, + description=description, + status="todo" + ) + db.add(task) + db.commit() + db.refresh(task) + return task + + +def get_task(db: Session, task_id: int): + return db.get(Task, task_id) + + +def list_tasks(db: Session, status: str | None = None, query: str | None = None): + stmt = select(Task) + + if status: + stmt = stmt.where(Task.status == status) + + if query: + q = f"%{query}%" + stmt = stmt.where( + Task.title.ilike(q) | + Task.description.ilike(q) + ) + + stmt = stmt.order_by(Task.created_at.desc()) + return db.scalars(stmt).all() + + +def update_task_status(db: Session, task_id: int, status: str): + task = db.get(Task, task_id) + if not task: + return None + task.status = status + db.commit() + db.refresh(task) + return task + + +def delete_task(db: Session, task_id: int): + task = db.get(Task, task_id) + if not task: + return False + db.delete(task) + db.commit() + return True diff --git a/app/db.py b/app/db.py new file mode 100644 index 00000000..ea1cd7c2 --- /dev/null +++ b/app/db.py @@ -0,0 +1,28 @@ +# Database setup and connection +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, DeclarativeBase + +DATABASE_URL = "sqlite:///./db.sqlite3" + +engine = create_engine( + DATABASE_URL, + connect_args={"check_same_thread": False} +) + +SessionLocal = sessionmaker( + autocommit=False, + autoflush=False, + bind=engine +) + + +class Base(DeclarativeBase): + pass + + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/app/main.py b/app/main.py new file mode 100644 index 00000000..262917be --- /dev/null +++ b/app/main.py @@ -0,0 +1,57 @@ +# Application entry point +from fastapi import FastAPI, Depends, HTTPException, status +from sqlalchemy.orm import Session + +from .db import Base, engine, get_db +from . import crud, schemas + +Base.metadata.create_all(bind=engine) + +app = FastAPI( + title="Task Manager API", + version="1.0.0" +) + + +@app.post("/tasks", response_model=schemas.TaskOut, status_code=status.HTTP_201_CREATED) +def create_task(payload: schemas.TaskCreate, db: Session = Depends(get_db)): + if not payload.title.strip(): + raise HTTPException(status_code=422, detail="title cannot be blank") + return crud.create_task(db, payload.title, payload.description) + + +@app.get("/tasks", response_model=list[schemas.TaskOut]) +def list_tasks( + status: schemas.TaskStatus | None = None, + query: str | None = None, + db: Session = Depends(get_db) +): + return crud.list_tasks(db, status=status, query=query) + + +@app.get("/tasks/{task_id}", response_model=schemas.TaskOut) +def get_task(task_id: int, db: Session = Depends(get_db)): + task = crud.get_task(db, task_id) + if not task: + raise HTTPException(status_code=404, detail="task not found") + return task + + +@app.patch("/tasks/{task_id}", response_model=schemas.TaskOut) +def update_task_status( + task_id: int, + payload: schemas.TaskUpdateStatus, + db: Session = Depends(get_db) +): + task = crud.update_task_status(db, task_id, payload.status) + if not task: + raise HTTPException(status_code=404, detail="task not found") + return task + + +@app.delete("/tasks/{task_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_task(task_id: int, db: Session = Depends(get_db)): + ok = crud.delete_task(db, task_id) + if not ok: + raise HTTPException(status_code=404, detail="task not found") + return None diff --git a/app/models.py b/app/models.py new file mode 100644 index 00000000..576d0489 --- /dev/null +++ b/app/models.py @@ -0,0 +1,21 @@ +# Data models +from datetime import datetime +from sqlalchemy import String, Text, DateTime +from sqlalchemy.orm import Mapped, mapped_column +from .db import Base + + +class Task(Base): + __tablename__ = "tasks" + + id: Mapped[int] = mapped_column(primary_key=True, index=True) + title: Mapped[str] = mapped_column(String(200), nullable=False) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + status: Mapped[str] = mapped_column(String(20), default="todo", nullable=False) + + created_at: Mapped[datetime] = mapped_column( + DateTime, default=datetime.utcnow + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, default=datetime.utcnow, onupdate=datetime.utcnow + ) diff --git a/app/schemas.py b/app/schemas.py new file mode 100644 index 00000000..27e36619 --- /dev/null +++ b/app/schemas.py @@ -0,0 +1,32 @@ +# Pydantic schemas +from datetime import datetime +from pydantic import BaseModel, Field +from typing import Optional, Literal + +TaskStatus = Literal["todo", "in_progress", "done"] + + +class TaskCreate(BaseModel): + title: str = Field(min_length=1, max_length=200) + description: Optional[str] = Field(default=None, max_length=2000) + + def model_post_init(self, __context): + self.title = self.title.strip() + if self.description is not None: + self.description = self.description.strip() + + +class TaskUpdateStatus(BaseModel): + status: TaskStatus + + +class TaskOut(BaseModel): + id: int + title: str + description: Optional[str] + status: TaskStatus + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..43a2932213250d14702447604810e45b0fc8f311 GIT binary patch literal 12288 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 UFTihWawg~z`Kczi7SMM4U$DT~asU7T literal 0 HcmV?d00001 diff --git a/tests/test_tasks.py b/tests/test_tasks.py new file mode 100644 index 00000000..0afacd50 --- /dev/null +++ b/tests/test_tasks.py @@ -0,0 +1,110 @@ +# Tests for tasks +import os +import tempfile +import pytest +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from app.db import Base, get_db +from app.main import app + + +@pytest.fixture() +def client(): + # create temp sqlite db for tests + fd, path = tempfile.mkstemp(suffix=".sqlite3") + os.close(fd) + + engine = create_engine( + f"sqlite:///{path}", + connect_args={"check_same_thread": False}, + ) + TestingSessionLocal = sessionmaker( + autocommit=False, + autoflush=False, + bind=engine, + ) + + Base.metadata.create_all(bind=engine) + + def override_get_db(): + db = TestingSessionLocal() + try: + yield db + finally: + db.close() + + app.dependency_overrides[get_db] = override_get_db + + with TestClient(app) as client: + yield client + + app.dependency_overrides.clear() + + engine.dispose() # <-- THIS LINE FIXES WINDOWS SQLITE LOCKING + + os.remove(path) + + +def test_create_and_get_task(client): + res = client.post( + "/tasks", + json={"title": "Buy milk", "description": "2 liters"}, + ) + assert res.status_code == 201 + + task = res.json() + assert task["title"] == "Buy milk" + assert task["status"] == "todo" + + task_id = task["id"] + + res = client.get(f"/tasks/{task_id}") + assert res.status_code == 200 + assert res.json()["id"] == task_id + + +def test_list_and_search_tasks(client): + client.post("/tasks", json={"title": "Alpha"}) + client.post("/tasks", json={"title": "Beta"}) + client.post("/tasks", json={"title": "Gamma"}) + + res = client.get("/tasks", params={"query": "bet"}) + assert res.status_code == 200 + + tasks = res.json() + assert len(tasks) == 1 + assert tasks[0]["title"] == "Beta" + + +def test_update_status(client): + res = client.post("/tasks", json={"title": "Test task"}) + task_id = res.json()["id"] + + res = client.patch( + f"/tasks/{task_id}", + json={"status": "in_progress"}, + ) + assert res.status_code == 200 + assert res.json()["status"] == "in_progress" + + +def test_delete_task(client): + res = client.post("/tasks", json={"title": "Delete me"}) + task_id = res.json()["id"] + + res = client.delete(f"/tasks/{task_id}") + assert res.status_code == 204 + + res = client.get(f"/tasks/{task_id}") + assert res.status_code == 404 + + +def test_404_cases(client): + assert client.get("/tasks/999").status_code == 404 + assert client.patch( + "/tasks/999", + json={"status": "done"}, + ).status_code == 404 + assert client.delete("/tasks/999").status_code == 404