From 80815bd2cdaf93154ee0224626b3b76e83bd5dc0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:35:44 +0000 Subject: [PATCH 1/2] Initial plan From 5d0c92c5a5670dfe15a9774ed44a794a501366cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:40:04 +0000 Subject: [PATCH 2/2] Address all PR review comments: dependency injection, idempotent seeding, main() guards, precision fix, tests Co-authored-by: goldlabelapps <244006120+goldlabelapps@users.noreply.github.com> --- app/api/routes.py | 73 ++++++++++++++++++---------------- app/main.py | 3 +- app/print_products.py | 38 ++++++++++-------- app/seed_product_table.py | 83 +++++++++++++++++++++------------------ app/test_db_connection.py | 67 +++++++++++++++++-------------- requirements.txt | 2 + tests/test_routes.py | 54 ++++++++++++++++++++++--- 7 files changed, 197 insertions(+), 123 deletions(-) diff --git a/app/api/routes.py b/app/api/routes.py index d023019..359212c 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -1,16 +1,35 @@ """API route definitions for NX AI.""" -from fastapi import APIRouter -from pydantic import BaseModel import os -from dotenv import load_dotenv +import time + import psycopg2 -router = APIRouter() +from dotenv import load_dotenv +from fastapi import APIRouter, Depends +from pydantic import BaseModel +from app import __version__ + +load_dotenv() router = APIRouter() +def get_db_connection(): # type: ignore[return] + """Create and yield a PostgreSQL connection for use as a FastAPI dependency.""" + conn = psycopg2.connect( + host=os.getenv('DB_HOST'), + port=os.getenv('DB_PORT', '5432'), + dbname=os.getenv('DB_NAME'), + user=os.getenv('DB_USER'), + password=os.getenv('DB_PASSWORD'), + ) + try: + yield conn + finally: + conn.close() + + class EchoRequest(BaseModel): """Request body for the echo endpoint.""" @@ -23,44 +42,32 @@ class EchoResponse(BaseModel): echo: str - -import time -import sys -from app import __version__ - @router.get("/") -def root() -> dict: +def root(conn=Depends(get_db_connection)) -> dict: """Return a structured welcome message for the API root, including product data.""" - load_dotenv() - conn = psycopg2.connect( - host=os.getenv('DB_HOST'), - port=os.getenv('DB_PORT', '5432'), - dbname=os.getenv('DB_NAME'), - user=os.getenv('DB_USER'), - password=os.getenv('DB_PASSWORD') - ) cur = conn.cursor() - cur.execute('SELECT id, name, description, price, in_stock, created_at FROM product;') - products = [ - { - "id": row[0], - "name": row[1], - "description": row[2], - "price": float(row[3]), - "in_stock": row[4], - "created_at": row[5].isoformat() if row[5] else None - } - for row in cur.fetchall() - ] - cur.close() - conn.close() + try: + cur.execute('SELECT id, name, description, price, in_stock, created_at FROM product;') + products = [ + { + "id": row[0], + "name": row[1], + "description": row[2], + "price": str(row[3]) if row[3] is not None else None, + "in_stock": row[4], + "created_at": row[5].isoformat() if row[5] else None, + } + for row in cur.fetchall() + ] + finally: + cur.close() epoch = int(time.time() * 1000) meta = { "version": __version__, "time": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), "epoch": epoch, "severity": "success", - "message": f"NX AI says hello. Returned {len(products)} products." + "message": f"NX AI says hello. Returned {len(products)} products.", } return {"meta": meta, "data": products} diff --git a/app/main.py b/app/main.py index c889a97..fbebbc9 100644 --- a/app/main.py +++ b/app/main.py @@ -2,12 +2,13 @@ from fastapi import FastAPI +from app import __version__ from app.api.routes import router app = FastAPI( title="NX AI", description="Production-ready Python FastAPI app for NX", - version="1.0.0", + version=__version__, ) app.include_router(router) diff --git a/app/print_products.py b/app/print_products.py index 01d6ca2..8428c6b 100644 --- a/app/print_products.py +++ b/app/print_products.py @@ -1,23 +1,29 @@ import os -from dotenv import load_dotenv + import psycopg2 +from dotenv import load_dotenv -load_dotenv() -conn = psycopg2.connect( - host=os.getenv('DB_HOST'), - port=os.getenv('DB_PORT', '5432'), - dbname=os.getenv('DB_NAME'), - user=os.getenv('DB_USER'), - password=os.getenv('DB_PASSWORD') -) -cur = conn.cursor() +def main() -> None: + load_dotenv() -cur.execute('SELECT * FROM product;') -rows = cur.fetchall() + conn = psycopg2.connect( + host=os.getenv('DB_HOST'), + port=os.getenv('DB_PORT', '5432'), + dbname=os.getenv('DB_NAME'), + user=os.getenv('DB_USER'), + password=os.getenv('DB_PASSWORD'), + ) + cur = conn.cursor() + try: + cur.execute('SELECT * FROM product;') + rows = cur.fetchall() + for row in rows: + print(row) + finally: + cur.close() + conn.close() -for row in rows: - print(row) -cur.close() -conn.close() +if __name__ == "__main__": + main() diff --git a/app/seed_product_table.py b/app/seed_product_table.py index 1b82ed9..9a23d12 100644 --- a/app/seed_product_table.py +++ b/app/seed_product_table.py @@ -1,41 +1,48 @@ import os -from dotenv import load_dotenv + import psycopg2 +from dotenv import load_dotenv + + +def main() -> None: + load_dotenv() + + conn = psycopg2.connect( + host=os.getenv('DB_HOST'), + port=os.getenv('DB_PORT', '5432'), + dbname=os.getenv('DB_NAME'), + user=os.getenv('DB_USER'), + password=os.getenv('DB_PASSWORD'), + ) + cur = conn.cursor() + try: + # Create product table with a unique constraint on name for idempotent seeding + cur.execute(''' + CREATE TABLE IF NOT EXISTS product ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL UNIQUE, + description TEXT, + price NUMERIC(10, 2) NOT NULL, + in_stock BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + ''') + + # Insert seed data; skip rows whose name already exists + cur.execute(''' + INSERT INTO product (name, description, price, in_stock) VALUES + ('Widget', 'A useful widget', 19.99, TRUE), + ('Gadget', 'A fancy gadget', 29.99, TRUE), + ('Thingamajig', 'An interesting thingamajig', 9.99, FALSE) + ON CONFLICT (name) DO NOTHING; + ''') + + conn.commit() + print("Product table created and seeded.") + finally: + cur.close() + conn.close() + -load_dotenv() - -# Database connection -conn = psycopg2.connect( - host=os.getenv('DB_HOST'), - port=os.getenv('DB_PORT', '5432'), - dbname=os.getenv('DB_NAME'), - user=os.getenv('DB_USER'), - password=os.getenv('DB_PASSWORD') -) -cur = conn.cursor() - -# Create product table -cur.execute(''' -CREATE TABLE IF NOT EXISTS product ( - id SERIAL PRIMARY KEY, - name VARCHAR(100) NOT NULL, - description TEXT, - price NUMERIC(10, 2) NOT NULL, - in_stock BOOLEAN DEFAULT TRUE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); -''') - -# Insert seed data -cur.execute(''' -INSERT INTO product (name, description, price, in_stock) VALUES - ('Widget', 'A useful widget', 19.99, TRUE), - ('Gadget', 'A fancy gadget', 29.99, TRUE), - ('Thingamajig', 'An interesting thingamajig', 9.99, FALSE) -ON CONFLICT DO NOTHING; -''') - -conn.commit() -cur.close() -conn.close() -print("Product table created and seeded.") +if __name__ == "__main__": + main() diff --git a/app/test_db_connection.py b/app/test_db_connection.py index 2da82e1..e6b738f 100644 --- a/app/test_db_connection.py +++ b/app/test_db_connection.py @@ -1,33 +1,40 @@ - -from dotenv import load_dotenv import os +import sys + import psycopg2 +from dotenv import load_dotenv + + +def main() -> int: + load_dotenv() + + db_host = os.getenv('DB_HOST') + db_port = os.getenv('DB_PORT', '5432') + db_name = os.getenv('DB_NAME') + db_user = os.getenv('DB_USER') + db_password = os.getenv('DB_PASSWORD') + + print("Attempting connection with:") + print(f"Host: {db_host}") + print(f"Port: {db_port}") + print(f"Database: {db_name}") + print(f"User: {db_user}") + + try: + conn = psycopg2.connect( + host=db_host, + port=db_port, + dbname=db_name, + user=db_user, + password=db_password, + ) + print("Connection successful!") + conn.close() + return 0 + except Exception as e: + print(f"Connection failed: {e}") + return 1 + -# Load .env file -load_dotenv() - -# Load environment variables -db_host = os.getenv('DB_HOST') -db_port = os.getenv('DB_PORT', '5432') -db_name = os.getenv('DB_NAME') -db_user = os.getenv('DB_USER') -db_password = os.getenv('DB_PASSWORD') - -print("Attempting connection with:") -print(f"Host: {db_host}") -print(f"Port: {db_port}") -print(f"Database: {db_name}") -print(f"User: {db_user}") - -try: - conn = psycopg2.connect( - host=db_host, - port=db_port, - dbname=db_name, - user=db_user, - password=db_password - ) - print("Connection successful!") - conn.close() -except Exception as e: - print(f"Connection failed: {e}") +if __name__ == "__main__": + sys.exit(main()) diff --git a/requirements.txt b/requirements.txt index 3f152f6..c06408c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,5 @@ fastapi>=0.110.0 uvicorn[standard]>=0.29.0 httpx>=0.27.0 pytest>=8.1.0 +python-dotenv>=1.0.0 +psycopg2-binary>=2.9.0 diff --git a/tests/test_routes.py b/tests/test_routes.py index 0e1686e..004c663 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -1,17 +1,61 @@ """Unit and integration tests for NX AI routes.""" +from unittest.mock import MagicMock + from fastapi.testclient import TestClient +from app.api.routes import get_db_connection from app.main import app client = TestClient(app) -def test_root_returns_welcome_message() -> None: - """GET / should return a welcome message.""" - response = client.get("/") - assert response.status_code == 200 - assert response.json() == {"message": "Welcome to NX AI!"} +def _mock_db_dependency(rows=None): + """Return a FastAPI dependency override that yields a mock DB connection.""" + if rows is None: + rows = [] + + def override(): + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_conn.cursor.return_value = mock_cursor + mock_cursor.fetchall.return_value = rows + yield mock_conn + + return override + + +def test_root_returns_product_data() -> None: + """GET / should return meta and data with product list.""" + app.dependency_overrides[get_db_connection] = _mock_db_dependency(rows=[]) + try: + response = client.get("/") + assert response.status_code == 200 + body = response.json() + assert "meta" in body + assert "data" in body + assert body["meta"]["severity"] == "success" + assert isinstance(body["data"], list) + finally: + app.dependency_overrides.clear() + + +def test_root_returns_products_from_db() -> None: + """GET / should include product rows returned by the database.""" + from datetime import datetime + from decimal import Decimal + mock_row = (1, "Widget", "A useful widget", Decimal("19.99"), True, datetime(2024, 1, 1, 0, 0, 0)) + app.dependency_overrides[get_db_connection] = _mock_db_dependency(rows=[mock_row]) + try: + response = client.get("/") + assert response.status_code == 200 + body = response.json() + assert len(body["data"]) == 1 + assert body["data"][0]["name"] == "Widget" + assert body["data"][0]["price"] == "19.99" + assert "Returned 1 products" in body["meta"]["message"] + finally: + app.dependency_overrides.clear() def test_health_returns_ok() -> None: