Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
__pycache__/
.venv/
.pytest_cache/

# IDE files
.idea/
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

++++++++++++++++++++++++++++++++++++++++++++++

## Local Setup and Usage

### Clone the Repository
```bash
https://github.com/jeevan-puli/spec-driven-development/tree/resource-management-api
```
check out the feature branch:
```bash
git checkout resource-management-api
```

### Create and Activate Virtual Environment (Python)
```bash
python3 -m venv .venv
source .venv/bin/activate
```
### Install Dependencies
```bash
pip install -r requirements.txt
```
### Run the Application
Start the FastAPI server locally:
```bash
uvicorn app.main:app --reload
```
Verify the API is running by opening:
```bash
http://127.0.0.1:8000/docs
```

### You can also manually verify the API:
```bash
curl -X POST http://127.0.0.1:8000/resources \
-H "Content-Type: application/json" \
-d '{"name":"example","type":"demo"}'
```
### Run Tests (without readable logs)
Execute the full test suite:
```bash
pytest
```
### Run Tests (with readable logs)
Execute the full test suite:
```bash
pytest --log-cli-level=INFO
```

## Code Generation Usage
Modern code generation tools (ChatGPT / Codex-style models) were used to
accelerate scaffolding of the API and test cases. All generated code was
reviewed and refined manually to ensure correctness, determinism, and
coverage of edge cases.
14 changes: 0 additions & 14 deletions SPECS/feature-template.md

This file was deleted.

33 changes: 33 additions & 0 deletions SPECS/resource-managment-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Feature Spec: Resource Management API

## Goal
Provide a simple backend API to create, retrieve, and filter resources in a spec-driven manner,
serving as a test target for API and contract testing.

## Scope
### In:
- Create a resource via API
- Retrieve all resources
- Filter resources by type
- Validate request and response schemas

### Out:
- Authentication / authorization
- External persistence (DB, cloud services)
- Frontend UI

## Requirements
- The API must support creating a resource with required fields.
- Each resource must be assigned a unique ID.
- Resources must be retrievable via a GET endpoint.
- Filtering by resource type must be supported.
- Invalid requests must return clear error responses.
- API responses must conform to a stable schema.

## Acceptance Criteria
- [ ] POST /resources creates a resource with valid input
- [ ] GET /resources returns all created resources
- [ ] GET /resources?type=<type> filters resources correctly
- [ ] Missing required fields return a 400 error
- [ ] Invalid resource type returns a validation error
- [ ] Response schema remains consistent across requests
4 changes: 2 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# TODO

## Refactor Proposals
-
- None at this time

## New Feature Proposals
-
- None at this time
Empty file added app/__init__.py
Empty file.
49 changes: 49 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from __future__ import annotations

from typing import Optional
from uuid import uuid4

from fastapi import FastAPI, Query
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from pydantic import BaseModel

app = FastAPI(title="Resource Management API")

# In-memory storage: id -> resource
resources: dict[str, "Resource"] = {}


class ResourceIn(BaseModel):
name: str
type: str


class Resource(ResourceIn):
id: str


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
return JSONResponse(status_code=400, content={"detail": "Invalid input"})


@app.post("/resources", response_model=Resource)
def create_resource(payload: ResourceIn) -> Resource:
resource = Resource(id=str(uuid4()), **payload.dict())
resources[resource.id] = resource
return resource


@app.get("/resources", response_model=list[Resource])
def list_resources(type: Optional[str] = Query(default=None)) -> list[Resource]:
items = list(resources.values())
if type is not None:
items = [r for r in items if r.type == type]
return items

@app.get("/health")
def health():
return {"status": "ok"}


3 changes: 3 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[pytest]
testpaths = tests
python_files = test_*.py
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
fastapi
uvicorn
pytest
httpx
6 changes: 6 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import sys
from pathlib import Path

# Add project root to PYTHONPATH
ROOT_DIR = Path(__file__).resolve().parents[1]
sys.path.append(str(ROOT_DIR))
119 changes: 119 additions & 0 deletions tests/test_resources_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import logging

logger = logging.getLogger(__name__)

from fastapi.testclient import TestClient

from app.main import app, resources

client = TestClient(app)


def setup_function():
# Ensure deterministic tests by clearing in-memory storage
resources.clear()


def test_create_resource_success():
logger.info("Creating resource with valid payload")

response = client.post(
"/resources",
json={"name": "resource-1", "type": "demo"},
)

logger.info("Create response status=%s body=%s", response.status_code, response.json())

assert response.status_code == 200
body = response.json()

assert "id" in body
assert body["name"] == "resource-1"
assert body["type"] == "demo"


def test_list_resources_returns_created_items():
logger.info("Creating multiple resources for list test")

client.post("/resources", json={"name": "r1", "type": "demo"})
client.post("/resources", json={"name": "r2", "type": "test"})

logger.info("Fetching all resources")

response = client.get("/resources")
logger.info("List response status=%s body=%s", response.status_code, response.json())

assert response.status_code == 200
body = response.json()

assert len(body) == 2
names = [item["name"] for item in body]
assert "r1" in names
assert "r2" in names


def test_filter_resources_by_type():
logger.info("Creating resources with different types")

client.post("/resources", json={"name": "r1", "type": "demo"})
client.post("/resources", json={"name": "r2", "type": "test"})

logger.info("Filtering resources by type=demo")

response = client.get("/resources", params={"type": "demo"})
logger.info("Filter response status=%s body=%s", response.status_code, response.json())

assert response.status_code == 200
body = response.json()

assert len(body) == 1
assert body[0]["name"] == "r1"
assert body[0]["type"] == "demo"


def test_create_resource_missing_required_fields_returns_400():
logger.info("Creating resource with missing required fields")

response = client.post("/resources", json={})
logger.info("Validation response status=%s body=%s", response.status_code, response.json())

assert response.status_code == 400
assert response.json() == {"detail": "Invalid input"}


def test_create_resource_invalid_payload_returns_400():
logger.info("Creating resource with invalid payload types")

response = client.post(
"/resources",
json={"name": 123, "type": ["not", "a", "string"]},
)

logger.info("Invalid payload response status=%s body=%s", response.status_code, response.json())

assert response.status_code == 400
assert response.json() == {"detail": "Invalid input"}


def test_response_schema_contains_expected_fields():
logger.info("Validating response schema for created resource")

response = client.post(
"/resources",
json={"name": "schema-test", "type": "demo"},
)

body = response.json()
logger.info("Schema validation response body=%s", body)

assert set(body.keys()) == {"id", "name", "type"}


def test_list_resources_returns_empty_list_when_no_resources_exist():
logger.info("Listing resources when no resources exist")

response = client.get("/resources")
logger.info("Empty list response status=%s body=%s", response.status_code, response.json())

assert response.status_code == 200
assert response.json() == []