diff --git a/README.md b/README.md index df4b987..ed8b006 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ # NX AI +> Production ready Python FastAPI/Postgres app for [NX](https://goldlabel.pro?s=nx-ai) AI services and more + ```sh uvicorn app.main:app +pytest ``` + [localhost](http://localhost:8000) [Public RESTful API](https://nx-ai.onrender.com) - -A clean, production-ready Python FastAPI app for [NX](https://goldlabel.pro?s=nx-ai) AI services and more - - **Python 3.11+** - **FastAPI** — RESTful API framework - **Uvicorn** — ASGI server @@ -70,8 +71,4 @@ curl -X POST http://localhost:8000/echo \ # {"echo":"hello"} ``` -## Running Tests -```bash -pytest -``` diff --git a/app/__init__.py b/app/__init__.py index 76f1818..7e7cac3 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,4 +1,4 @@ """NX AI - FastAPI""" # Version tracking -__version__ = "1.0.0" +__version__ = "1.0.2" diff --git a/app/api/routes.py b/app/api/routes.py index 784a160..359212c 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -1,11 +1,35 @@ """API route definitions for NX AI.""" -from fastapi import APIRouter +import os +import time + +import psycopg2 +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.""" @@ -18,23 +42,34 @@ class EchoResponse(BaseModel): echo: str - -import time -import sys -from app import __version__ - @router.get("/") -def root() -> dict: - """Return a structured welcome message for the API root.""" +def root(conn=Depends(get_db_connection)) -> dict: + """Return a structured welcome message for the API root, including product data.""" + cur = conn.cursor() + 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": "NX AI says hello.", + "message": f"NX AI says hello. Returned {len(products)} products.", } - return {"meta": meta} + return {"meta": meta, "data": products} @router.get("/health") diff --git a/app/main.py b/app/main.py index c540707..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="A clean, modular FastAPI application for AI services.", - version="1.0.0", + description="Production-ready Python FastAPI app for NX", + version=__version__, ) app.include_router(router) diff --git a/app/print_products.py b/app/print_products.py new file mode 100644 index 0000000..8428c6b --- /dev/null +++ b/app/print_products.py @@ -0,0 +1,29 @@ +import os + +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: + cur.execute('SELECT * FROM product;') + rows = cur.fetchall() + for row in rows: + print(row) + finally: + cur.close() + conn.close() + + +if __name__ == "__main__": + main() diff --git a/app/seed_product_table.py b/app/seed_product_table.py new file mode 100644 index 0000000..9a23d12 --- /dev/null +++ b/app/seed_product_table.py @@ -0,0 +1,48 @@ +import os + +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() + + +if __name__ == "__main__": + main() diff --git a/app/test_db_connection.py b/app/test_db_connection.py new file mode 100644 index 0000000..e6b738f --- /dev/null +++ b/app/test_db_connection.py @@ -0,0 +1,40 @@ +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 + + +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..afc7aad 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -1,7 +1,10 @@ """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) @@ -11,7 +14,11 @@ 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!"} + json_data = response.json() + assert "meta" in json_data + assert "data" in json_data + assert "message" in json_data["meta"] + assert "NX AI" in json_data["meta"]["message"] def test_health_returns_ok() -> None: