Skip to content

Commit 0a09b43

Browse files
Merge pull request #9 from goldlabelapps/staging
This pull request introduces a PostgreSQL-backed product catalog to the NX AI FastAPI app, enhances the root endpoint to return product data, and adds supporting scripts and tests for database integration.
2 parents c75348d + f1fbe74 commit 0a09b43

9 files changed

Lines changed: 180 additions & 21 deletions

File tree

README.md

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
# NX AI
22

3+
> Production ready Python FastAPI/Postgres app for [NX](https://goldlabel.pro?s=nx-ai) AI services and more
4+
35
```sh
46
uvicorn app.main:app
7+
pytest
58
```
9+
610
[localhost](http://localhost:8000)
711
[Public RESTful API](https://nx-ai.onrender.com)
812

9-
10-
A clean, production-ready Python FastAPI app for [NX](https://goldlabel.pro?s=nx-ai) AI services and more
11-
1213
- **Python 3.11+**
1314
- **FastAPI** — RESTful API framework
1415
- **Uvicorn** — ASGI server
@@ -70,8 +71,4 @@ curl -X POST http://localhost:8000/echo \
7071
# {"echo":"hello"}
7172
```
7273

73-
## Running Tests
7474

75-
```bash
76-
pytest
77-
```

app/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
"""NX AI - FastAPI"""
22

33
# Version tracking
4-
__version__ = "1.0.0"
4+
__version__ = "1.0.2"

app/api/routes.py

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,35 @@
11
"""API route definitions for NX AI."""
22

3-
from fastapi import APIRouter
3+
import os
4+
import time
5+
6+
import psycopg2
7+
from dotenv import load_dotenv
8+
from fastapi import APIRouter, Depends
49
from pydantic import BaseModel
510

11+
from app import __version__
12+
13+
load_dotenv()
14+
615
router = APIRouter()
716

817

18+
def get_db_connection(): # type: ignore[return]
19+
"""Create and yield a PostgreSQL connection for use as a FastAPI dependency."""
20+
conn = psycopg2.connect(
21+
host=os.getenv('DB_HOST'),
22+
port=os.getenv('DB_PORT', '5432'),
23+
dbname=os.getenv('DB_NAME'),
24+
user=os.getenv('DB_USER'),
25+
password=os.getenv('DB_PASSWORD'),
26+
)
27+
try:
28+
yield conn
29+
finally:
30+
conn.close()
31+
32+
933
class EchoRequest(BaseModel):
1034
"""Request body for the echo endpoint."""
1135

@@ -18,23 +42,34 @@ class EchoResponse(BaseModel):
1842
echo: str
1943

2044

21-
22-
import time
23-
import sys
24-
from app import __version__
25-
2645
@router.get("/")
27-
def root() -> dict:
28-
"""Return a structured welcome message for the API root."""
46+
def root(conn=Depends(get_db_connection)) -> dict:
47+
"""Return a structured welcome message for the API root, including product data."""
48+
cur = conn.cursor()
49+
try:
50+
cur.execute('SELECT id, name, description, price, in_stock, created_at FROM product;')
51+
products = [
52+
{
53+
"id": row[0],
54+
"name": row[1],
55+
"description": row[2],
56+
"price": str(row[3]) if row[3] is not None else None,
57+
"in_stock": row[4],
58+
"created_at": row[5].isoformat() if row[5] else None,
59+
}
60+
for row in cur.fetchall()
61+
]
62+
finally:
63+
cur.close()
2964
epoch = int(time.time() * 1000)
3065
meta = {
3166
"version": __version__,
3267
"time": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()),
3368
"epoch": epoch,
3469
"severity": "success",
35-
"message": "NX AI says hello.",
70+
"message": f"NX AI says hello. Returned {len(products)} products.",
3671
}
37-
return {"meta": meta}
72+
return {"meta": meta, "data": products}
3873

3974

4075
@router.get("/health")

app/main.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
from fastapi import FastAPI
44

5+
from app import __version__
56
from app.api.routes import router
67

78
app = FastAPI(
89
title="NX AI",
9-
description="A clean, modular FastAPI application for AI services.",
10-
version="1.0.0",
10+
description="Production-ready Python FastAPI app for NX",
11+
version=__version__,
1112
)
1213

1314
app.include_router(router)

app/print_products.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import os
2+
3+
import psycopg2
4+
from dotenv import load_dotenv
5+
6+
7+
def main() -> None:
8+
load_dotenv()
9+
10+
conn = psycopg2.connect(
11+
host=os.getenv('DB_HOST'),
12+
port=os.getenv('DB_PORT', '5432'),
13+
dbname=os.getenv('DB_NAME'),
14+
user=os.getenv('DB_USER'),
15+
password=os.getenv('DB_PASSWORD'),
16+
)
17+
cur = conn.cursor()
18+
try:
19+
cur.execute('SELECT * FROM product;')
20+
rows = cur.fetchall()
21+
for row in rows:
22+
print(row)
23+
finally:
24+
cur.close()
25+
conn.close()
26+
27+
28+
if __name__ == "__main__":
29+
main()

app/seed_product_table.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import os
2+
3+
import psycopg2
4+
from dotenv import load_dotenv
5+
6+
7+
def main() -> None:
8+
load_dotenv()
9+
10+
conn = psycopg2.connect(
11+
host=os.getenv('DB_HOST'),
12+
port=os.getenv('DB_PORT', '5432'),
13+
dbname=os.getenv('DB_NAME'),
14+
user=os.getenv('DB_USER'),
15+
password=os.getenv('DB_PASSWORD'),
16+
)
17+
cur = conn.cursor()
18+
try:
19+
# Create product table with a unique constraint on name for idempotent seeding
20+
cur.execute('''
21+
CREATE TABLE IF NOT EXISTS product (
22+
id SERIAL PRIMARY KEY,
23+
name VARCHAR(100) NOT NULL UNIQUE,
24+
description TEXT,
25+
price NUMERIC(10, 2) NOT NULL,
26+
in_stock BOOLEAN DEFAULT TRUE,
27+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
28+
);
29+
''')
30+
31+
# Insert seed data; skip rows whose name already exists
32+
cur.execute('''
33+
INSERT INTO product (name, description, price, in_stock) VALUES
34+
('Widget', 'A useful widget', 19.99, TRUE),
35+
('Gadget', 'A fancy gadget', 29.99, TRUE),
36+
('Thingamajig', 'An interesting thingamajig', 9.99, FALSE)
37+
ON CONFLICT (name) DO NOTHING;
38+
''')
39+
40+
conn.commit()
41+
print("Product table created and seeded.")
42+
finally:
43+
cur.close()
44+
conn.close()
45+
46+
47+
if __name__ == "__main__":
48+
main()

app/test_db_connection.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import os
2+
import sys
3+
4+
import psycopg2
5+
from dotenv import load_dotenv
6+
7+
8+
def main() -> int:
9+
load_dotenv()
10+
11+
db_host = os.getenv('DB_HOST')
12+
db_port = os.getenv('DB_PORT', '5432')
13+
db_name = os.getenv('DB_NAME')
14+
db_user = os.getenv('DB_USER')
15+
db_password = os.getenv('DB_PASSWORD')
16+
17+
print("Attempting connection with:")
18+
print(f"Host: {db_host}")
19+
print(f"Port: {db_port}")
20+
print(f"Database: {db_name}")
21+
print(f"User: {db_user}")
22+
23+
try:
24+
conn = psycopg2.connect(
25+
host=db_host,
26+
port=db_port,
27+
dbname=db_name,
28+
user=db_user,
29+
password=db_password,
30+
)
31+
print("Connection successful!")
32+
conn.close()
33+
return 0
34+
except Exception as e:
35+
print(f"Connection failed: {e}")
36+
return 1
37+
38+
39+
if __name__ == "__main__":
40+
sys.exit(main())

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ fastapi>=0.110.0
22
uvicorn[standard]>=0.29.0
33
httpx>=0.27.0
44
pytest>=8.1.0
5+
python-dotenv>=1.0.0
6+
psycopg2-binary>=2.9.0

tests/test_routes.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
"""Unit and integration tests for NX AI routes."""
22

3+
from unittest.mock import MagicMock
4+
35
from fastapi.testclient import TestClient
46

7+
from app.api.routes import get_db_connection
58
from app.main import app
69

710
client = TestClient(app)
@@ -11,7 +14,11 @@ def test_root_returns_welcome_message() -> None:
1114
"""GET / should return a welcome message."""
1215
response = client.get("/")
1316
assert response.status_code == 200
14-
assert response.json() == {"message": "Welcome to NX AI!"}
17+
json_data = response.json()
18+
assert "meta" in json_data
19+
assert "data" in json_data
20+
assert "message" in json_data["meta"]
21+
assert "NX AI" in json_data["meta"]["message"]
1522

1623

1724
def test_health_returns_ok() -> None:

0 commit comments

Comments
 (0)