From 5d1ed489fc4bd3cbacb80b81c5c7d1600bff73b0 Mon Sep 17 00:00:00 2001 From: psiri0514-cyber Date: Mon, 9 Feb 2026 21:50:46 -0500 Subject: [PATCH] Update README and add task management API spec --- README.md | 57 +++++++++- SPECS/task-management-api.md | 40 +++++++ app.py | 119 +++++++++++++++++++++ requirements.txt | 4 + test_app.py | 196 +++++++++++++++++++++++++++++++++++ 5 files changed, 415 insertions(+), 1 deletion(-) create mode 100644 SPECS/task-management-api.md create mode 100644 app.py create mode 100644 requirements.txt create mode 100644 test_app.py diff --git a/README.md b/README.md index 494f1c75..344254c7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Candidate Assessment: Spec-Driven Development With Codegen Tools -This assessment evaluates how you use modern code generation tools (for example `5.2-Codex`, `Claude`, `Copilot`, and similar) to design, build, and test a software application using a spec-driven development pattern. You may build a frontend, a backend, or both. +This assessment evaluates how you use modern code generation tools to design, build, and test a software application using a spec-driven development pattern. You may build a frontend, a backend, or both. ## Goals - Build a working application with at least one meaningful feature. @@ -41,3 +41,58 @@ 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 Management API + +This is a simple REST API for managing tasks. You can create, read, update, and delete tasks. It's built with Python and Flask. + +## Setup + +Make sure you have Python installed, then run: + +```bash +pip install -r requirements.txt +``` + +## Running the App + +Start the server: + +```bash +python app.py +``` + +The API runs on `http://localhost:5000` + +## API Endpoints + +- `GET /` - Check if API is running +- `POST /api/tasks` - Create a new task +- `GET /api/tasks` - Get all tasks +- `GET /api/tasks/{id}` - Get one task +- `PUT /api/tasks/{id}` - Update a task +- `DELETE /api/tasks/{id}` - Delete a task + +## Example + +Create a task: +```bash +curl -X POST http://localhost:5000/api/tasks \ + -H "Content-Type: application/json" \ + -d '{"title": "My task", "description": "Do something"}' +``` + +## Running Tests + +Run all tests: + +```bash +pytest +``` + +## Notes + +- Tasks are stored in memory and will be lost when you restart the server +- All responses are in JSON format diff --git a/SPECS/task-management-api.md b/SPECS/task-management-api.md new file mode 100644 index 00000000..15013ab3 --- /dev/null +++ b/SPECS/task-management-api.md @@ -0,0 +1,40 @@ +# Feature Spec: Task Management API + +## Goal +- Build a RESTful backend API for managing tasks +- Provide CRUD operations (Create, Read, Update, Delete) for tasks +- Include data persistence (in-memory storage) +- Create a comprehensive test suite + +## Scope +- In: + - REST API endpoints for task management + - Task model with id, title, description, status, and created_at fields + - In-memory data storage + - Test suite with unit and integration tests + - API documentation +- Out: + - Frontend UI + - Database persistence (using in-memory storage instead) + - Authentication/Authorization + - Advanced features like filtering, pagination + +## Requirements +- API must be built using Python and Flask framework +- All endpoints must return JSON responses +- Tasks should have: id (auto-generated), title (required), description (optional), status (default: "pending"), created_at (timestamp) +- API should handle errors gracefully with appropriate HTTP status codes +- Tests must cover all endpoints and edge cases +- Application must run locally with simple setup + +## Acceptance Criteria +- [ ] POST /api/tasks - Create a new task +- [ ] GET /api/tasks - List all tasks +- [ ] GET /api/tasks/{id} - Get a specific task by ID +- [ ] PUT /api/tasks/{id} - Update a task by ID +- [ ] DELETE /api/tasks/{id} - Delete a task by ID +- [ ] All endpoints return appropriate HTTP status codes (200, 201, 404, 400) +- [ ] Test suite covers all endpoints with success and error cases +- [ ] Application can be run with simple command (e.g., `python app.py` or `flask run`) +- [ ] README includes setup and run instructions +- [ ] Tests can be run with a simple command (e.g., `pytest`) diff --git a/app.py b/app.py new file mode 100644 index 00000000..8174eb1c --- /dev/null +++ b/app.py @@ -0,0 +1,119 @@ +from flask import Flask, request, jsonify +from flask_cors import CORS +from datetime import datetime +import uuid + +app = Flask(__name__) +CORS(app) + +tasks = [] + + +class Task: + # Initialize task with title, optional description and status + def __init__(self, title, description=None, status="pending"): + self.id = str(uuid.uuid4()) + self.title = title + self.description = description or "" + self.status = status + self.created_at = datetime.utcnow().isoformat() + + # Convert task object to dictionary + def to_dict(self): + return { + "id": self.id, + "title": self.title, + "description": self.description, + "status": self.status, + "created_at": self.created_at + } + + # Update task fields from provided data + def update(self, data): + if "title" in data: + self.title = data["title"] + if "description" in data: + self.description = data["description"] + if "status" in data: + self.status = data["status"] + + +# Find and return task by ID +def find_task_by_id(task_id): + for task in tasks: + if task.id == task_id: + return task + return None + + +# Create a new task +@app.route("/api/tasks", methods=["POST"]) +def create_task(): + data = request.get_json() + + if not data or "title" not in data: + return jsonify({"error": "Title is required"}), 400 + + task = Task( + title=data["title"], + description=data.get("description"), + status=data.get("status", "pending") + ) + tasks.append(task) + + return jsonify(task.to_dict()), 201 + + +# Get all tasks +@app.route("/api/tasks", methods=["GET"]) +def list_tasks(): + return jsonify([task.to_dict() for task in tasks]), 200 + + +# Get a specific task by ID +@app.route("/api/tasks/", methods=["GET"]) +def get_task(task_id): + task = find_task_by_id(task_id) + + if not task: + return jsonify({"error": "Task not found"}), 404 + + return jsonify(task.to_dict()), 200 + + +# Update a task by ID +@app.route("/api/tasks/", methods=["PUT"]) +def update_task(task_id): + task = find_task_by_id(task_id) + + if not task: + return jsonify({"error": "Task not found"}), 404 + + data = request.get_json() + if not data: + return jsonify({"error": "No data provided"}), 400 + + task.update(data) + return jsonify(task.to_dict()), 200 + + +# Delete a task by ID +@app.route("/api/tasks/", methods=["DELETE"]) +def delete_task(task_id): + task = find_task_by_id(task_id) + + if not task: + return jsonify({"error": "Task not found"}), 404 + + tasks.remove(task) + return jsonify({"message": "Task deleted successfully"}), 200 + + +# Health check endpoint +@app.route("/", methods=["GET"]) +def health_check(): + return jsonify({"status": "ok", "message": "Task Management API is running"}), 200 + + +if __name__ == "__main__": + app.run(debug=True, host="0.0.0.0", port=5000) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..b402f7c5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +Flask==3.0.0 +pytest==7.4.3 +pytest-flask==1.3.0 +flask-cors==4.0.0 diff --git a/test_app.py b/test_app.py new file mode 100644 index 00000000..32a833e9 --- /dev/null +++ b/test_app.py @@ -0,0 +1,196 @@ +import pytest +from app import app, tasks, Task + + +@pytest.fixture +def client(): + app.config["TESTING"] = True + with app.test_client() as client: + tasks.clear() + yield client + + +def test_health_check(client): + response = client.get("/") + assert response.status_code == 200 + data = response.get_json() + assert data["status"] == "ok" + + +def test_create_task_success(client): + response = client.post( + "/api/tasks", + json={"title": "Test Task", "description": "This is a test"} + ) + assert response.status_code == 201 + data = response.get_json() + assert data["title"] == "Test Task" + assert data["description"] == "This is a test" + assert data["status"] == "pending" + assert "id" in data + assert "created_at" in data + + +def test_create_task_with_status(client): + response = client.post( + "/api/tasks", + json={"title": "Completed Task", "status": "completed"} + ) + assert response.status_code == 201 + data = response.get_json() + assert data["status"] == "completed" + + +def test_create_task_missing_title(client): + response = client.post( + "/api/tasks", + json={"description": "No title"} + ) + assert response.status_code == 400 + data = response.get_json() + assert "error" in data + + +def test_create_task_empty_json(client): + response = client.post("/api/tasks", json={}) + assert response.status_code == 400 + + +def test_list_tasks_empty(client): + response = client.get("/api/tasks") + assert response.status_code == 200 + data = response.get_json() + assert data == [] + + +def test_list_tasks_with_data(client): + client.post("/api/tasks", json={"title": "Task 1"}) + client.post("/api/tasks", json={"title": "Task 2"}) + + response = client.get("/api/tasks") + assert response.status_code == 200 + data = response.get_json() + assert len(data) == 2 + assert data[0]["title"] == "Task 1" + assert data[1]["title"] == "Task 2" + + +def test_get_task_success(client): + create_response = client.post( + "/api/tasks", + json={"title": "Get Me", "description": "Find this task"} + ) + task_id = create_response.get_json()["id"] + + response = client.get(f"/api/tasks/{task_id}") + assert response.status_code == 200 + data = response.get_json() + assert data["title"] == "Get Me" + assert data["description"] == "Find this task" + assert data["id"] == task_id + + +def test_get_task_not_found(client): + response = client.get("/api/tasks/non-existent-id") + assert response.status_code == 404 + data = response.get_json() + assert "error" in data + + +def test_update_task_success(client): + create_response = client.post( + "/api/tasks", + json={"title": "Original Title", "status": "pending"} + ) + task_id = create_response.get_json()["id"] + + response = client.put( + f"/api/tasks/{task_id}", + json={"title": "Updated Title", "status": "completed", "description": "New description"} + ) + assert response.status_code == 200 + data = response.get_json() + assert data["title"] == "Updated Title" + assert data["status"] == "completed" + assert data["description"] == "New description" + + +def test_update_task_partial(client): + create_response = client.post( + "/api/tasks", + json={"title": "Original", "description": "Original desc"} + ) + task_id = create_response.get_json()["id"] + + response = client.put( + f"/api/tasks/{task_id}", + json={"title": "New Title"} + ) + assert response.status_code == 200 + data = response.get_json() + assert data["title"] == "New Title" + assert data["description"] == "Original desc" + + +def test_update_task_not_found(client): + response = client.put( + "/api/tasks/non-existent-id", + json={"title": "Updated"} + ) + assert response.status_code == 404 + + +def test_update_task_no_data(client): + create_response = client.post("/api/tasks", json={"title": "Test"}) + task_id = create_response.get_json()["id"] + + response = client.put(f"/api/tasks/{task_id}", json={}) + assert response.status_code == 400 + + +def test_delete_task_success(client): + create_response = client.post("/api/tasks", json={"title": "To Delete"}) + task_id = create_response.get_json()["id"] + + response = client.delete(f"/api/tasks/{task_id}") + assert response.status_code == 200 + data = response.get_json() + assert "message" in data + + get_response = client.get(f"/api/tasks/{task_id}") + assert get_response.status_code == 404 + + +def test_delete_task_not_found(client): + response = client.delete("/api/tasks/non-existent-id") + assert response.status_code == 404 + + +def test_full_workflow(client): + task1_response = client.post("/api/tasks", json={"title": "Workflow Task 1"}) + task2_response = client.post("/api/tasks", json={"title": "Workflow Task 2"}) + + task1_id = task1_response.get_json()["id"] + task2_id = task2_response.get_json()["id"] + + list_response = client.get("/api/tasks") + assert list_response.status_code == 200 + assert len(list_response.get_json()) == 2 + + get_response = client.get(f"/api/tasks/{task1_id}") + assert get_response.status_code == 200 + assert get_response.get_json()["title"] == "Workflow Task 1" + + update_response = client.put( + f"/api/tasks/{task1_id}", + json={"status": "completed"} + ) + assert update_response.status_code == 200 + assert update_response.get_json()["status"] == "completed" + + delete_response = client.delete(f"/api/tasks/{task1_id}") + assert delete_response.status_code == 200 + + list_response = client.get("/api/tasks") + assert len(list_response.get_json()) == 1 + assert list_response.get_json()[0]["id"] == task2_id