From b83993f9a0699230bc3abc4ac516b6cf933ab226 Mon Sep 17 00:00:00 2001 From: Goldlabel Apps Ltd Date: Mon, 23 Mar 2026 12:41:47 +0000 Subject: [PATCH 1/3] Use CSV-backed schema and DB helper Replace direct psycopg2/dotenv usage with the shared get_db_connection helper; product listing now SELECTs * and builds dicts from cursor.description to return the full CSV-derived schema. The reset endpoint now recreates a product table matching the CSV columns and seeds rows from tests/csv/small.csv using csv.DictReader with parameterized INSERTs. Updated docstrings and response messages accordingly and removed the previous hardcoded sample seed data. --- app/api/products/products.py | 38 +++++----------- app/api/products/reset.py | 88 ++++++++++++++++++++---------------- 2 files changed, 59 insertions(+), 67 deletions(-) diff --git a/app/api/products/products.py b/app/api/products/products.py index e44f9a4..aae963e 100644 --- a/app/api/products/products.py +++ b/app/api/products/products.py @@ -1,43 +1,27 @@ + from app import __version__ from fastapi import APIRouter -from fastapi import status import os, time -import psycopg2 -from dotenv import load_dotenv -from app import __version__ +from app.api.db import get_db_connection router = APIRouter() @router.get("/products") def root() -> 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') - ) + """Return all products with full CSV-based schema.""" + conn_gen = get_db_connection() + conn = next(conn_gen) 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.execute('SELECT * FROM product;') + if cur.description is None: + products = [] + else: + columns = [desc[0] for desc in cur.description] + products = [dict(zip(columns, row)) for row in cur.fetchall()] cur.close() conn.close() - load_dotenv() base_url = os.getenv("BASE_URL", "http://localhost:8000") - epoch = int(time.time() * 1000) meta = { "severity": "success", diff --git a/app/api/products/reset.py b/app/api/products/reset.py index b1bfa74..ae0529c 100644 --- a/app/api/products/reset.py +++ b/app/api/products/reset.py @@ -1,60 +1,68 @@ import os -import psycopg2 -from dotenv import load_dotenv +import csv from fastapi import APIRouter, status +from app.api.db import get_db_connection router = APIRouter() +CSV_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'tests', 'csv', 'small.csv') + @router.post("/products/reset", status_code=status.HTTP_200_OK) def reset_products() -> dict: - """Delete and recreate the product table, then seed with initial 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') - ) + """Delete and recreate the product table, then seed with CSV data.""" + conn_gen = get_db_connection() + conn = next(conn_gen) cur = conn.cursor() - # Drop and recreate table + # Drop and recreate table with all CSV columns cur.execute(''' DROP TABLE IF EXISTS product; CREATE TABLE product ( id SERIAL PRIMARY KEY, - name VARCHAR(255) NOT NULL, - description TEXT, - price NUMERIC(10,2) NOT NULL, - in_stock BOOLEAN DEFAULT TRUE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + Params TEXT, + item INTEGER, + desc TEXT, + UOS TEXT, + Pack_Description TEXT, + Hierarchy1 TEXT, + Hierarchy2 TEXT, + Hierarchy3 TEXT, + UOP TEXT, + sSell1 NUMERIC(10,2), + sSell2 NUMERIC(10,2), + sSell3 NUMERIC(10,2), + sSell4 NUMERIC(10,2), + sSell5 NUMERIC(10,2), + pack1 INTEGER, + pack2 INTEGER, + pack3 INTEGER, + pack4 INTEGER, + pack5 INTEGER, + EAN TEXT ); ''') - # Seed data - seed_products = [ - ("Apple", "Fresh red apple", 0.99, True), - ("Banana", "Organic banana", 0.59, True), - ("Orange", "Juicy orange", 1.29, True), - ] - cur.executemany( - "INSERT INTO product (name, description, price, in_stock) VALUES (%s, %s, %s, %s);", - seed_products - ) + # Read and insert CSV data + with open(CSV_PATH, newline='') as csvfile: + reader = csv.DictReader(csvfile) + rows = [row for row in reader] + for row in rows: + cur.execute( + """ + INSERT INTO product ( + Params, item, desc, UOS, Pack_Description, Hierarchy1, Hierarchy2, Hierarchy3, UOP, + sSell1, sSell2, sSell3, sSell4, sSell5, pack1, pack2, pack3, pack4, pack5, EAN + ) VALUES ( + %(Params)s, %(item)s, %(desc)s, %(UOS)s, %(Pack_Description)s, %(Hierarchy1)s, %(Hierarchy2)s, %(Hierarchy3)s, %(UOP)s, + %(sSell1)s, %(sSell2)s, %(sSell3)s, %(sSell4)s, %(sSell5)s, %(pack1)s, %(pack2)s, %(pack3)s, %(pack4)s, %(pack5)s, %(EAN)s + ) + """, + row + ) conn.commit() - 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.execute('SELECT * FROM product;') + products = [dict(zip([desc[0] for desc in cur.description], row)) for row in cur.fetchall()] cur.close() conn.close() - return {"message": "Product table reset and seeded.", "data": products} + return {"message": "Product table reset and seeded from CSV.", "data": products} import os, time import psycopg2 from dotenv import load_dotenv From 0948dab1d2d29afc7e2e175260316a4832901204 Mon Sep 17 00:00:00 2001 From: Goldlabel Apps Ltd Date: Mon, 23 Mar 2026 14:37:15 +0000 Subject: [PATCH 2/3] Update favicon.ico --- app/static/favicon.ico | Bin 15086 -> 1150 bytes 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 app/static/favicon.ico diff --git a/app/static/favicon.ico b/app/static/favicon.ico old mode 100644 new mode 100755 index 05442c468e24a4d601b61cd6c0fcf754efbb3c2d..8e4f67149f4d19d068757f1d4f07e22484ef53d4 GIT binary patch literal 1150 zcmZQzU<5(|0R|wcz>vYhz#zuJz@P!dKp~(AL>x#lFaYJy!To07aAJ63fb*YHpt#wbi0uGt5>fM z5fBh~2U0I2B=lB6LE#<<>*?uTSh#RuEV5d3_O4yK+*nvxe(C7wTsm^(h|Qlrf0%dd z*x`QZ(j`4~wdj11S``(QTOdEadGkgXBwt@&-w1LC$jnoxPMM##QY2lKjax0J^;m^|7T!0Q_sL~2Z-+hF#yeGg$Muu literal 15086 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 Date: Mon, 23 Mar 2026 15:05:16 +0000 Subject: [PATCH 3/3] Rename reset to seed and seed products from CSV Rename products/reset.py to products/seed.py and update API to seed the product table from an in-package CSV. Change endpoint from POST /products/reset to GET /products/seed, move CSV to app/api/products/start_data.csv and resolve its absolute path. Map CSV 'desc' field to DB 'title' on insert, update SQL to use title, and guard against empty query results (handle cur.description None). Update imports and router includes to use products_router/seed_router names. Remove legacy product-list root logic and clean up tests by deleting obsolete echo tests. --- app/api/products/__init__.py | 4 +- app/api/products/{reset.py => seed.py} | 62 +++++-------------- .../api/products/start_data.csv | 0 app/api/routes.py | 4 +- tests/test_routes.py | 19 ------ 5 files changed, 21 insertions(+), 68 deletions(-) rename app/api/products/{reset.py => seed.py} (55%) rename tests/csv/small.csv => app/api/products/start_data.csv (100%) diff --git a/app/api/products/__init__.py b/app/api/products/__init__.py index 057f7eb..475ce29 100644 --- a/app/api/products/__init__.py +++ b/app/api/products/__init__.py @@ -1,2 +1,2 @@ -from .products import router -from .reset import router as reset_router +from .products import router as products_router +from .seed import router as seed_router diff --git a/app/api/products/reset.py b/app/api/products/seed.py similarity index 55% rename from app/api/products/reset.py rename to app/api/products/seed.py index ae0529c..53fee19 100644 --- a/app/api/products/reset.py +++ b/app/api/products/seed.py @@ -1,14 +1,16 @@ -import os +from app import __version__ +import os, time import csv from fastapi import APIRouter, status from app.api.db import get_db_connection router = APIRouter() -CSV_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'tests', 'csv', 'small.csv') +CSV_PATH = os.path.join(os.path.dirname(__file__), 'start_data.csv') +CSV_PATH = os.path.abspath(CSV_PATH) -@router.post("/products/reset", status_code=status.HTTP_200_OK) -def reset_products() -> dict: +@router.get("/products/seed", status_code=status.HTTP_200_OK) +def seed_products() -> dict: """Delete and recreate the product table, then seed with CSV data.""" conn_gen = get_db_connection() conn = next(conn_gen) @@ -20,7 +22,7 @@ def reset_products() -> dict: id SERIAL PRIMARY KEY, Params TEXT, item INTEGER, - desc TEXT, + title TEXT, UOS TEXT, Pack_Description TEXT, Hierarchy1 TEXT, @@ -45,13 +47,15 @@ def reset_products() -> dict: reader = csv.DictReader(csvfile) rows = [row for row in reader] for row in rows: + # Map 'desc' from CSV to 'title' for DB + row['title'] = row.pop('desc') cur.execute( """ INSERT INTO product ( - Params, item, desc, UOS, Pack_Description, Hierarchy1, Hierarchy2, Hierarchy3, UOP, + Params, item, title, UOS, Pack_Description, Hierarchy1, Hierarchy2, Hierarchy3, UOP, sSell1, sSell2, sSell3, sSell4, sSell5, pack1, pack2, pack3, pack4, pack5, EAN ) VALUES ( - %(Params)s, %(item)s, %(desc)s, %(UOS)s, %(Pack_Description)s, %(Hierarchy1)s, %(Hierarchy2)s, %(Hierarchy3)s, %(UOP)s, + %(Params)s, %(item)s, %(title)s, %(UOS)s, %(Pack_Description)s, %(Hierarchy1)s, %(Hierarchy2)s, %(Hierarchy3)s, %(UOP)s, %(sSell1)s, %(sSell2)s, %(sSell3)s, %(sSell4)s, %(sSell5)s, %(pack1)s, %(pack2)s, %(pack3)s, %(pack4)s, %(pack5)s, %(EAN)s ) """, @@ -59,51 +63,19 @@ def reset_products() -> dict: ) conn.commit() cur.execute('SELECT * FROM product;') - products = [dict(zip([desc[0] for desc in cur.description], row)) for row in cur.fetchall()] + if cur.description is None: + products = [] + else: + columns = [desc[0] for desc in cur.description] + products = [dict(zip(columns, row)) for row in cur.fetchall()] cur.close() conn.close() - return {"message": "Product table reset and seeded from CSV.", "data": products} -import os, time -import psycopg2 -from dotenv import load_dotenv -from app import __version__ - -router = APIRouter() -@router.get("/products") -def root() -> 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() - - load_dotenv() base_url = os.getenv("BASE_URL", "http://localhost:8000") - epoch = int(time.time() * 1000) meta = { "severity": "success", - "title": "Product List", + "title": "Product table seeded", "version": __version__, "base_url": base_url, "time": epoch, diff --git a/tests/csv/small.csv b/app/api/products/start_data.csv similarity index 100% rename from tests/csv/small.csv rename to app/api/products/start_data.csv diff --git a/app/api/routes.py b/app/api/routes.py index d89d651..ec04a6f 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -13,9 +13,9 @@ from app.api.root import router as root_router from app.api.health import router as health_router from app.api.products.products import router as products_router -from app.api.products.reset import router as reset_router +from app.api.products.seed import router as seed_router router.include_router(root_router) router.include_router(health_router) router.include_router(products_router) -router.include_router(reset_router) +router.include_router(seed_router) diff --git a/tests/test_routes.py b/tests/test_routes.py index b3431c9..3437436 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -25,22 +25,3 @@ def test_health_returns_ok() -> None: assert response.status_code == 200 assert response.json() == {"status": "ok"} - -def test_echo_returns_message() -> None: - """POST /echo should return the provided message.""" - response = client.post("/echo", json={"message": "hello"}) - assert response.status_code == 200 - assert response.json() == {"echo": "hello"} - - -def test_echo_empty_string() -> None: - """POST /echo with an empty string should echo back an empty string.""" - response = client.post("/echo", json={"message": ""}) - assert response.status_code == 200 - assert response.json() == {"echo": ""} - - -def test_echo_missing_body_returns_422() -> None: - """POST /echo without a body should return 422 Unprocessable Entity.""" - response = client.post("/echo", json={}) - assert response.status_code == 422