This pull request introduces PostgreSQL database integration for product data management and retrieval in the NX AI FastAPI application. The main changes include adding scripts for database setup and testing, modifying the root API endpoint to return product data, and updating documentation and metadata for clarity.#7
This pull request introduces PostgreSQL database integration for product data management and retrieval in the NX AI FastAPI application. The main changes include adding scripts for database setup and testing, modifying the root API endpoint to return product data, and updating documentation and metadata for clarity.#7goldlabelapps wants to merge 10 commits intomasterfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds PostgreSQL-backed product storage/retrieval to the NX AI FastAPI app, including scripts to initialize/inspect the database and updating the root endpoint to return product data.
Changes:
- Added PostgreSQL utility scripts to test connectivity, create/seed the
producttable, and print product rows. - Updated
GET /to query PostgreSQL and return product data with enrichedmeta. - Updated app metadata/docs and bumped package version.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| app/api/routes.py | Root endpoint now loads env, connects to Postgres, queries product, and returns {meta, data} |
| app/main.py | Updated FastAPI description string |
| app/init.py | Bumped __version__ to 1.0.1 |
| app/seed_product_table.py | New script to create product table and insert seed data |
| app/print_products.py | New script to print all rows from product |
| app/test_db_connection.py | New script to test DB connectivity from env vars |
| README.md | Minor wording tweak for “production-ready” description |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| app = FastAPI( | ||
| title="NX AI", | ||
| description="A clean, modular FastAPI application for AI services.", | ||
| description="Production-ready Python FastAPI app for NX", | ||
| version="1.0.0", | ||
| ) |
There was a problem hiding this comment.
FastAPI(..., version="1.0.0") is now out of sync with app.__version__ (bumped to 1.0.1). Consider sourcing the FastAPI version from app.__version__ so API docs and metadata stay consistent.
| 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; |
There was a problem hiding this comment.
ON CONFLICT DO NOTHING won't prevent duplicate seed rows here because the table has no unique constraint that these inserts can conflict on (the serial id isn't provided). Add a unique constraint (e.g., on name) and/or use an upsert target so re-running the script is idempotent.
| 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.") |
There was a problem hiding this comment.
This script opens a DB connection at import time. Wrap the logic in a main() and guard it with if __name__ == "__main__": so importing the module doesn't create/seed tables unexpectedly (and so errors can be handled more cleanly).
| 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.") | |
| def main() -> None: | |
| 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() | |
| try: | |
| # 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() | |
| print("Product table created and seeded.") | |
| finally: | |
| cur.close() | |
| conn.close() | |
| if __name__ == "__main__": | |
| main() |
| router = APIRouter() | ||
|
|
||
|
|
||
| router = APIRouter() |
There was a problem hiding this comment.
router = APIRouter() is declared twice; the first assignment is redundant and makes it easy to accidentally register routes on the wrong router instance. Keep a single router instance definition.
| import os | ||
| from dotenv import load_dotenv | ||
| import psycopg2 |
There was a problem hiding this comment.
This module now hard-depends on python-dotenv and psycopg2, but the repo's requirements.txt currently doesn't include them. Add the corresponding dependencies (e.g., python-dotenv and psycopg2-binary/psycopg2) so imports don't fail at runtime.
| "id": row[0], | ||
| "name": row[1], | ||
| "description": row[2], | ||
| "price": float(row[3]), |
There was a problem hiding this comment.
Casting NUMERIC to float can lose precision for prices. Prefer returning a Decimal-safe representation (e.g., str(row[3])) or use a serialization approach that preserves exact values.
| "price": float(row[3]), | |
| "price": str(row[3]) if row[3] is not None else None, |
| "message": f"NX AI says hello. Returned {len(products)} products." | ||
| } | ||
| return {"meta": meta} | ||
| return {"meta": meta, "data": products} |
There was a problem hiding this comment.
The root response shape changed from the previous simple welcome message to {meta, data} and now depends on a live DB connection; the existing tests/test_routes.py::test_root_returns_welcome_message will fail (and CI may fail without a DB). Update tests and/or make DB access injectable/mockable so unit tests don't require external PostgreSQL.
| 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 * FROM product;') | ||
| rows = cur.fetchall() | ||
|
|
||
| for row in rows: | ||
| print(row) | ||
|
|
||
| cur.close() | ||
| conn.close() |
There was a problem hiding this comment.
This script opens a DB connection at import time. Wrap the logic in a main() and guard it with if __name__ == "__main__": to avoid side effects if the module is imported (and to make error handling/exit codes cleaner).
| 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 * FROM product;') | |
| rows = cur.fetchall() | |
| for row in rows: | |
| print(row) | |
| cur.close() | |
| conn.close() | |
| def main(): | |
| 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 * FROM product;') | |
| rows = cur.fetchall() | |
| for row in rows: | |
| print(row) | |
| cur.close() | |
| conn.close() | |
| if __name__ == "__main__": | |
| main() |
|
|
||
| # 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}") |
There was a problem hiding this comment.
This script executes connection attempts and prints connection details at import time. Wrap it in a main() with an if __name__ == "__main__": guard so importing the module doesn't trigger network access/printing (and so it can return a non-zero exit code on failure).
| # 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}") | |
| import sys | |
| def main() -> int: | |
| # 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() | |
| return 0 | |
| except Exception as e: | |
| print(f"Connection failed: {e}") | |
| return 1 | |
| if __name__ == "__main__": | |
| sys.exit(main()) |
| cur.close() | ||
| conn.close() |
There was a problem hiding this comment.
cur.close()/conn.close() won't run if an exception occurs between connection creation and these lines, which can leak connections/cursors. Wrap DB operations in try/finally (or context managers) and return an appropriate error response when the query fails.
|
@copilot open a new pull request to apply changes based on the comments in this thread |
|
@goldlabelapps I've opened a new pull request, #8, to work on those changes. Once the pull request is ready, I'll request review from you. |
…ing, main() guards, precision fix, tests Co-authored-by: goldlabelapps <244006120+goldlabelapps@users.noreply.github.com>
Fix DB integration quality: dependency injection, idempotent seeding, price precision, and test isolation
Database integration and management:
app/seed_product_table.pyto create theproducttable and seed it with initial data in PostgreSQL.app/print_products.pyto print all products from the database for debugging and verification purposes.app/test_db_connection.pyto test database connectivity using environment variables.API enhancements:
app/api/routes.pyto connect to PostgreSQL, retrieve product data, and return it in the API response, including a count of products in the meta message.app/api/routes.pyto support database connectivity and environment variable loading.Documentation and metadata updates:
README.mdandapp/main.pyto clarify that the app is production-ready and to reflect the new database integration. [1] [2]app/__init__.pyto1.0.1to reflect these changes.