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
32 changes: 28 additions & 4 deletions SPECS/feature-template.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,38 @@
# Feature Spec: <Feature Name>
# Feature Spec: Stock Retrieval

## Goal
-
- Create a simple fullstack application that allows a user to view stock prices. The application will have a backend API that retrieves stock prices from a third-party API and a frontend that displays the stock prices to the user.

## Scope
- In:
- Backend API to retrieve stock prices from a third-party API.
- Frontend to display stock prices to the user.
- Testing framework for both frontend and backend.
- Documentation on how to run the application and tests.
- Out:
- User authentication and authorization
- Portfolio tracking or watchlists
- Real-time stock price updates via WebSocket
- Historical price charting

## Requirements
-
- Backend must expose a REST API endpoint to fetch stock prices
- Frontend must allow users to search for stocks by ticker symbol
- Stock data should include price, change percentage, and timestamp
- API responses should be cached to minimize third-party API calls
- Error handling for invalid ticker symbols and API failures
- Application should run on localhost with documented ports
- Unit tests with minimum 80% code coverage for backend
- Component tests for React frontend components
- Must be ran with docker

## Acceptance Criteria
- [ ]
- [ ] Backend API returns stock price data in JSON format
- [ ] Frontend displays stock ticker, current price, and price change
- [ ] Users can search for stocks by entering a ticker symbol
- [ ] Invalid ticker symbols display an appropriate error message
- [ ] API failures gracefully handle and display error to user
- [ ] Backend tests pass with 80% or greater coverage
- [ ] Frontend component tests pass
- [ ] Submission_README includes setup instructions for both frontend and backend as well as explain my thought process.
- [ ] Application runs successfully on `http://localhost:5050` (backend) and `http://localhost:3000` (frontend)
22 changes: 22 additions & 0 deletions Submission_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Candidate Assessment: Spec-Driven Development With Codegen Tools

## Approach
- I wanted to create a simple application that allowed a user to create an account, login, and view a dashboard that allows them to look at stock prices.
- I choose to use Claude 4.6 Opus for code generation, as it has a good understanding of both frontend and backend development.
- I started by defining the specifications for the application, including the user stories and the API endpoints.
- I then used Claude to generate the initial code for both the frontend and backend, and iteratively refined the code based on testing and feedback.
- I also created a test suite to validate the functionality of the application, including unit tests for the backend and integration tests for the frontend.

## How To Run the Application
1. Clone the repository to your local machine.
2. Navigate to the project directory and install the dependencies for both the frontend and backend.
- For the frontend, run `npm install` in the `frontend` directory.
- For the backend, run `pip install -r requirements.txt` in the `backend` directory.
3. You can run the full application by running docker-compose up from the root directory, which will start both the frontend and backend services.
4. Alternatively, you can run the frontend and backend separately:
- To run the backend, navigate to the `backend` directory and run `python run.py`. This will start the backend server on `http://localhost:5050`.
- To Run the frontend, navigate to the `frontend` directory and run `npm start`. This will start the frontend development server on `http://localhost:3000`.
5. Once both servers are running, you can access the application by navigating to `http://localhost:3000` in your web browser.
6. You can create an account, login, and view the dashboard to see stock prices. The application is designed to be simple and easy to use, with a focus on demonstrating the capabilities of code generation tools in a spec-driven development process.
- If you have any issues pulling a stock price, make sure you disable any ad blockers or browser extensions that might interfere with API calls. For example PI Hole interferes with the stock price API calls, so you may need to disable it temporarily to see the stock prices in the dashboard.
7. To run the tests, navigate to the `backend` directory and run `python -m pytest tests/ -v`. This will execute the test suite and provide feedback on the functionality of the backend API endpoints. You can also run it with coverage using `python -m pytest tests/ -v --cov=app --cov-report=term-missing`. You can test the frontend by navigating to the `frontend` directory and running `npm test -- --watchAll=false` or `npm test -- --watchAll=false --coverage` for coverage.
12 changes: 12 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM python:3.9.6-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 5050

CMD ["python", "run.py"]
Empty file added backend/__init__.py
Empty file.
32 changes: 32 additions & 0 deletions backend/app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from flask import Flask
from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy
from flask_jwt_extended import JWTManager

db = SQLAlchemy()
jwt = JWTManager()


def create_app(config_class=None):
app = Flask(__name__)

if config_class:
app.config.from_object(config_class)
else:
from app.config import Config
app.config.from_object(Config)

CORS(app, resources={r"/api/*": {"origins": "*"}})
db.init_app(app)
jwt.init_app(app)

from app.auth import auth_bp
from app.stock import stock_bp

app.register_blueprint(auth_bp, url_prefix="/api/auth")
app.register_blueprint(stock_bp, url_prefix="/api/stock")

with app.app_context():
db.create_all()

return app
71 changes: 71 additions & 0 deletions backend/app/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from flask import Blueprint, request, jsonify
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity
from app import db
from app.models import User

auth_bp = Blueprint("auth", __name__)


@auth_bp.route("/register", methods=["POST"])
def register():
data = request.get_json()

if not data:
return jsonify({"error": "No data provided"}), 400

username = data.get("username")
email = data.get("email")
password = data.get("password")

if not username or not email or not password:
return jsonify({"error": "Username, email, and password are required"}), 400

if len(password) < 6:
return jsonify({"error": "Password must be at least 6 characters"}), 400

if User.query.filter_by(username=username).first():
return jsonify({"error": "Username already exists"}), 409

if User.query.filter_by(email=email).first():
return jsonify({"error": "Email already registered"}), 409

user = User(username=username, email=email)
user.set_password(password)
db.session.add(user)
db.session.commit()

return jsonify({"message": "User registered successfully", "user": user.to_dict()}), 201


@auth_bp.route("/login", methods=["POST"])
def login():
data = request.get_json()

if not data:
return jsonify({"error": "No data provided"}), 400

username = data.get("username")
password = data.get("password")

if not username or not password:
return jsonify({"error": "Username and password are required"}), 400

user = User.query.filter_by(username=username).first()

if not user or not user.check_password(password):
return jsonify({"error": "Invalid username or password"}), 401

access_token = create_access_token(identity=str(user.id))
return jsonify({"access_token": access_token, "user": user.to_dict()}), 200


@auth_bp.route("/me", methods=["GET"])
@jwt_required()
def get_current_user():
user_id = get_jwt_identity()
user = User.query.get(int(user_id))

if not user:
return jsonify({"error": "User not found"}), 404

return jsonify({"user": user.to_dict()}), 200
14 changes: 14 additions & 0 deletions backend/app/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import os


class Config:
SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret-key-change-in-production")
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", "sqlite:///app.db")
SQLALCHEMY_TRACK_MODIFICATIONS = False
JWT_SECRET_KEY = os.environ.get("JWT_SECRET_KEY", "jwt-secret-change-in-production")


class TestConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
JWT_SECRET_KEY = "test-jwt-secret"
24 changes: 24 additions & 0 deletions backend/app/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from app import db
from werkzeug.security import generate_password_hash, check_password_hash


class User(db.Model):
__tablename__ = "users"

id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(256), nullable=False)

def set_password(self, password):
self.password_hash = generate_password_hash(password)

def check_password(self, password):
return check_password_hash(self.password_hash, password)

def to_dict(self):
return {
"id": self.id,
"username": self.username,
"email": self.email,
}
51 changes: 51 additions & 0 deletions backend/app/stock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required
import yfinance as yf

stock_bp = Blueprint("stock", __name__)


def fetch_stock_price(symbol):
"""Fetch current stock price for a given symbol using yfinance."""
ticker = yf.Ticker(symbol)
info = ticker.fast_info

current_price = getattr(info, "last_price", None)
previous_close = getattr(info, "previous_close", None)
market_cap = getattr(info, "market_cap", None)

if current_price is None and previous_close is None:
return None

return {
"symbol": symbol.upper(),
"price": round(current_price, 2) if current_price else None,
"previous_close": round(previous_close, 2) if previous_close else None,
"market_cap": market_cap,
"currency": getattr(info, "currency", "USD"),
}


@stock_bp.route("/price", methods=["GET"])
@jwt_required()
def get_stock_price():
symbol = request.args.get("symbol")

if not symbol:
return jsonify({"error": "Stock symbol is required"}), 400

symbol = symbol.strip().upper()

if not symbol.isalpha() or len(symbol) > 5:
return jsonify({"error": "Invalid stock symbol"}), 400

try:
stock_data = fetch_stock_price(symbol)

if stock_data is None:
return jsonify({"error": f"Could not find stock data for {symbol}"}), 404

return jsonify({"stock": stock_data}), 200

except Exception as e:
return jsonify({"error": f"Failed to fetch stock data: {str(e)}"}), 500
8 changes: 8 additions & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Flask==2.3.3
Flask-Cors==4.0.0
Flask-SQLAlchemy==3.1.1
Flask-JWT-Extended==4.6.0
Werkzeug==2.3.7
yfinance==1.1.0
pytest==7.4.3
pytest-cov==4.1.0
6 changes: 6 additions & 0 deletions backend/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from app import create_app

app = create_app()

if __name__ == "__main__":
app.run(host="0.0.0.0", port=5050, debug=True)
1 change: 1 addition & 0 deletions backend/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

39 changes: 39 additions & 0 deletions backend/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import pytest
from app import create_app, db
from app.config import TestConfig
from app.models import User


@pytest.fixture
def app():
app = create_app(TestConfig)
with app.app_context():
db.create_all()
yield app
db.session.remove()
db.drop_all()


@pytest.fixture
def client(app):
return app.test_client()


@pytest.fixture
def sample_user(app):
with app.app_context():
user = User(username="testuser", email="test@example.com")
user.set_password("password123")
db.session.add(user)
db.session.commit()
return user.to_dict()


@pytest.fixture
def auth_header(client, sample_user):
response = client.post(
"/api/auth/login",
json={"username": "testuser", "password": "password123"},
)
token = response.get_json()["access_token"]
return {"Authorization": f"Bearer {token}"}
Loading