diff --git a/.gitignore b/.gitignore index e69de29b..464beeeb 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1 @@ +notepad .gitignore \ No newline at end of file diff --git a/README.md b/README.md index 494f1c75..8cbbd493 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,108 @@ # 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. + +- Build a working application with at least one feature. - Create a testing framework to validate the application. - Demonstrate effective use of code generation tools to accelerate delivery. -- Show clear, maintainable engineering practices. ## Deliverables + - Application source code in this repository. - A test suite and test harness that can be run locally. - Documentation that explains how to run the app and the tests. ## Scope Options -Pick one: + +Pick one of the following: + - Frontend-only application. - Backend-only application. - Full-stack application. -Your solution should include at least one real workflow, for example: -- Create and view a resource. -- Search or filter data. -- Persist data in memory or storage. - ## Rules -- You must use a code generation tool (for example `5.2-Codex`, `Claude`, or similar). You can use multiple tools. + +- You must use a code generation tool (for example Codex, Claude, or similar). You can use multiple tools. - You must build the application and a testing framework for it. - The application and tests must run locally. - Do not include secrets or credentials in this repository. ## Evaluation Criteria + - Working product: Does the app do what it claims? - Test coverage: Do tests cover key workflows and edge cases? - Engineering quality: Clarity, structure, and maintainability. - Use of codegen: How effectively you used tools to accelerate work. - Documentation: Clear setup and run instructions. -## What to Submit +## Submission Criteria + - 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. +- Include a short summary of your approach and tools used in your PR submission. +- Include any additional information or approach that helped you. + +--- + +# Task Management API + +REST API for managing tasks: create, read, update, and delete. Implemented in Python with Flask. + +## Setup + +Requires Python 3. Install dependencies: + +```bash +pip install -r requirements.txt +``` + +## Running the App + +Start the server: + +```bash +python app.py +``` + +The API is available at `http://localhost:5000`. + +## API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/` | Health check; returns status and message. | +| POST | `/api/tasks` | Create a task. Body must include `title`; `description` and `status` are optional. | +| GET | `/api/tasks` | List all tasks. | +| GET | `/api/tasks/{id}` | Get a single task by ID. | +| PUT | `/api/tasks/{id}` | Update a task. Body may include `title`, `description`, `status`. | +| DELETE | `/api/tasks/{id}` | Delete a task by ID. | + +## Example Request + +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 the full test suite: + +```bash +pytest +``` + +For verbose output: + +```bash +pytest -v +``` + +## Notes + +- Tasks are stored in memory only; data is lost when the server stops. +- All request and response bodies are JSON. 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/__pycache__/app.cpython-311.pyc b/__pycache__/app.cpython-311.pyc new file mode 100644 index 00000000..8f037206 Binary files /dev/null and b/__pycache__/app.cpython-311.pyc differ diff --git a/__pycache__/test_app.cpython-311-pytest-8.2.1.pyc b/__pycache__/test_app.cpython-311-pytest-8.2.1.pyc new file mode 100644 index 00000000..ba5af72b Binary files /dev/null and b/__pycache__/test_app.cpython-311-pytest-8.2.1.pyc differ diff --git a/app.py b/app.py new file mode 100644 index 00000000..96815d50 --- /dev/null +++ b/app.py @@ -0,0 +1,140 @@ +from flask import Flask, request, jsonify +from flask_cors import CORS +from datetime import datetime +import uuid + +app = Flask(__name__) +CORS(app) + + +# Holds items in a map by id and a list for ordered listing; supports clear() for tests +class TaskRepo: + def __init__(self): + self._by_id = {} + self._order = [] + + def clear(self): + self._by_id.clear() + self._order.clear() + + def add(self, item): + uid = item["id"] + self._by_id[uid] = item + self._order.append(uid) + + def get(self, tid): + return self._by_id.get(tid) + + def drop(self, item): + uid = item["id"] + del self._by_id[uid] + self._order.remove(uid) + + def list_all(self): + return [self._by_id[uid] for uid in self._order] + + +tasks = TaskRepo() + + +# Build one new item dict +def _build_item(title, description=None, status="pending"): + uid = str(uuid.uuid4()) + return { + "id": uid, + "title": title, + "description": description if description is not None else "", + "status": status, + "created_at": datetime.utcnow().isoformat(), + } + + +# Mutate item with payload keys +def _patch(item, payload): + for key in ("title", "description", "status"): + if key in payload: + item[key] = payload[key] + + +# Public Task type for code that imports it +class Task: + 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() + + def to_dict(self): + return { + "id": self.id, + "title": self.title, + "description": self.description, + "status": self.status, + "created_at": self.created_at, + } + + 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"] + + +@app.route("/api/tasks", methods=["POST"]) +def create_task(): + body = request.get_json() + if not body or "title" not in body: + return jsonify({"error": "Title is required"}), 400 + entry = _build_item( + title=body["title"], + description=body.get("description"), + status=body.get("status", "pending"), + ) + tasks.add(entry) + return jsonify(entry), 201 + + +@app.route("/api/tasks", methods=["GET"]) +def list_tasks(): + return jsonify(tasks.list_all()), 200 + + +@app.route("/api/tasks/", methods=["GET"]) +def get_task(task_id): + entry = tasks.get(task_id) + if entry is None: + return jsonify({"error": "Task not found"}), 404 + return jsonify(entry), 200 + + +@app.route("/api/tasks/", methods=["PUT"]) +def update_task(task_id): + entry = tasks.get(task_id) + if entry is None: + return jsonify({"error": "Task not found"}), 404 + body = request.get_json() + if not body: + return jsonify({"error": "No data provided"}), 400 + _patch(entry, body) + return jsonify(entry), 200 + + +@app.route("/api/tasks/", methods=["DELETE"]) +def delete_task(task_id): + entry = tasks.get(task_id) + if entry is None: + return jsonify({"error": "Task not found"}), 404 + tasks.drop(entry) + return jsonify({"message": "Task deleted successfully"}), 200 + + +@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/cd b/cd new file mode 100644 index 00000000..e69de29b diff --git a/git b/git new file mode 100644 index 00000000..e69de29b 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