Skip to content

Commit fab4165

Browse files
committed
Add orders API, importer scripts and data
Bump app version to 2.2.1 and add a new orders API package plus tooling. Introduces a GET /orders endpoint with pagination, search (case-insensitive partial match), hide/flag filtering, ordering and pagination metadata. Adds SQL utilities: a check_orders_table script for quick row/sample inspection and an import_magento_products_to_orders script to ingest magento_products.csv into the orders table (uses ON CONFLICT (sku) DO NOTHING). Also adds a large magento_products.csv sample data file and wires the orders router into the API routes/root updates. Note: importer assumes column mapping in ORDERS_COLUMNS matches the DB schema and performs basic type conversions for booleans, numerics and dates; review schema alignment before running.
1 parent 093497b commit fab4165

9 files changed

Lines changed: 4552 additions & 6 deletions

app/__init__.py

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

33
# Current Version
4-
__version__ = "2.2.0"
4+
__version__ = "2.2.1"

app/api/orders/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""Prospect Routes"""
2+
3+
from .orders import router as orders_router
4+
# from .flag import router as flag_router

app/api/orders/orders.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from app import __version__
2+
import os
3+
from app.utils.make_meta import make_meta
4+
from fastapi import APIRouter, Query, Path, Body, HTTPException
5+
from app.utils.db import get_db_connection
6+
7+
router = APIRouter()
8+
base_url = os.getenv("BASE_URL", "http://localhost:8000")
9+
10+
11+
# Refactored GET /orders endpoint to return paginated, filtered, and ordered results
12+
@router.get("/orders")
13+
def get_orders(
14+
page: int = Query(1, ge=1, description="Page number (1-based)"),
15+
limit: int = Query(100, ge=1, le=500, description="Records per page (default 100, max 500)"),
16+
search: str = Query(None, description="Search string (case-insensitive, partial match)"),
17+
hideflagged: bool = Query(False, description="If true, flagged records are excluded")
18+
) -> dict:
19+
"""Return paginated, filtered, and ordered records, filtered by search if provided."""
20+
meta = make_meta("success", "Read paginated orders")
21+
conn_gen = get_db_connection()
22+
conn = next(conn_gen)
23+
cur = conn.cursor()
24+
offset = (page - 1) * limit
25+
try:
26+
# Build WHERE clause
27+
where_clauses = ["hide IS NOT TRUE"]
28+
params = []
29+
if hideflagged:
30+
where_clauses.append("flag IS NOT TRUE")
31+
if search:
32+
where_clauses.append("(LOWER(first_name) LIKE %s OR LOWER(last_name) LIKE %s)")
33+
search_param = f"%{search.lower()}%"
34+
params.extend([search_param, search_param])
35+
where_sql = " AND ".join(where_clauses)
36+
37+
# Count query
38+
count_query = f'SELECT COUNT(*) FROM orders WHERE {where_sql};'
39+
cur.execute(count_query, params)
40+
count_row = cur.fetchone() if cur.description is not None else None
41+
total = count_row[0] if count_row is not None else 0
42+
43+
# Data query
44+
data_query = f'''
45+
SELECT * FROM prospects
46+
WHERE {where_sql}
47+
ORDER BY first_name ASC
48+
OFFSET %s LIMIT %s;
49+
'''
50+
cur.execute(data_query, params + [offset, limit])
51+
if cur.description is not None:
52+
columns = [desc[0] for desc in cur.description]
53+
rows = cur.fetchall()
54+
data = [dict(zip(columns, row)) for row in rows]
55+
else:
56+
data = []
57+
except Exception as e:
58+
data = []
59+
total = 0
60+
meta = make_meta("error", f"Failed to read prospects: {str(e)}")
61+
finally:
62+
cur.close()
63+
conn.close()
64+
return {
65+
"meta": meta,
66+
"pagination": {
67+
"page": page,
68+
"limit": limit,
69+
"total": total,
70+
"pages": (total // limit) + (1 if total % limit else 0)
71+
},
72+
"data": data,
73+
}
74+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""
2+
Script to check the number of rows in the orders table and print a sample row.
3+
"""
4+
from app.utils.db import get_db_connection_direct
5+
6+
def check_orders_table():
7+
conn = get_db_connection_direct()
8+
with conn:
9+
with conn.cursor() as cur:
10+
cur.execute("SELECT COUNT(*) FROM orders;")
11+
count = cur.fetchone()[0]
12+
print(f"orders table row count: {count}")
13+
if count > 0:
14+
cur.execute("SELECT * FROM orders LIMIT 1;")
15+
row = cur.fetchone()
16+
print("Sample row:")
17+
print(row)
18+
print("Check complete.")
19+
20+
if __name__ == "__main__":
21+
check_orders_table()
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""
2+
Script to import data from magento_products.csv into the orders table.
3+
"""
4+
import csv
5+
from datetime import datetime
6+
from app.utils.db import get_db_connection_direct
7+
8+
CSV_PATH = "app/static/csv/magento_products.csv"
9+
10+
# List of columns in the orders table (must match the table definition)
11+
ORDERS_COLUMNS = [
12+
"sku","store_view_code","attribute_set_code","product_type","categories","product_websites","name","description","short_description","weight","product_online","tax_class_name","visibility","price","special_price","special_price_from_date","special_price_to_date","url_key","meta_title","meta_keywords","meta_description","base_image","base_image_label","small_image","small_image_label","thumbnail_image","thumbnail_image_label","swatch_image","swatch_image_label","created_at","updated_at","new_from_date","new_to_date","display_product_options_in","map_price","msrp_price","map_enabled","gift_message_available","custom_design","custom_design_from","custom_design_to","custom_layout_update","page_layout","product_options_container","msrp_display_actual_price_type","country_of_manufacture","additional_attributes","qty","out_of_stock_qty","use_config_min_qty","is_qty_decimal","allow_backorders","use_config_backorders","min_cart_qty","use_config_min_sale_qty","max_cart_qty","use_config_max_sale_qty","is_in_stock","notify_on_stock_below","use_config_notify_stock_qty","manage_stock","use_config_manage_stock","use_config_qty_increments","qty_increments","use_config_enable_qty_inc","enable_qty_increments","is_decimal_divided","website_id","related_skus","related_position","crosssell_skus","crosssell_position","upsell_skus","upsell_position","additional_images","additional_image_labels","hide_from_product_page","custom_options","bundle_price_type","bundle_sku_type","bundle_price_view","bundle_weight_type","bundle_values","bundle_shipment_type","associated_skus","downloadable_links","downloadable_samples","configurable_variations","configurable_variation_labels","hide","flag"
13+
]
14+
15+
16+
def import_csv_to_orders():
17+
conn = get_db_connection_direct()
18+
inserted = 0
19+
total = 0
20+
with conn:
21+
with conn.cursor() as cur:
22+
with open(CSV_PATH, newline='', encoding='utf-8') as csvfile:
23+
reader = csv.DictReader(csvfile)
24+
for idx, row in enumerate(reader):
25+
total += 1
26+
# Add hide and flag fields if not present
27+
row.setdefault("hide", False)
28+
row.setdefault("flag", False)
29+
# Convert empty strings to None
30+
values = [row.get(col) if row.get(col) != '' else None for col in ORDERS_COLUMNS]
31+
# Print first row and values for debug
32+
if idx == 0:
33+
print("First CSV row:", row)
34+
print("First values list:", values)
35+
# Convert booleans and numerics as needed
36+
for i, col in enumerate(ORDERS_COLUMNS):
37+
if col in ["hide", "flag", "product_online", "use_config_min_qty", "is_qty_decimal", "allow_backorders", "use_config_backorders", "use_config_min_sale_qty", "use_config_max_sale_qty", "is_in_stock", "notify_on_stock_below", "use_config_notify_stock_qty", "manage_stock", "use_config_manage_stock", "use_config_qty_increments", "use_config_enable_qty_inc", "enable_qty_increments", "is_decimal_divided"]:
38+
if values[i] is not None:
39+
values[i] = str(values[i]).lower() in ("1", "true", "t", "yes")
40+
elif col in ["weight", "price", "special_price", "map_price", "msrp_price", "qty", "out_of_stock_qty", "min_cart_qty", "max_cart_qty", "qty_increments"]:
41+
if values[i] is not None:
42+
try:
43+
values[i] = float(values[i])
44+
except ValueError:
45+
values[i] = None
46+
elif col.endswith("_date") or col.endswith("_at") or col in ["custom_design_from", "custom_design_to"]:
47+
if values[i] is not None:
48+
try:
49+
# Try parsing as date or datetime
50+
values[i] = datetime.strptime(values[i], "%m/%d/%y")
51+
except Exception:
52+
try:
53+
values[i] = datetime.strptime(values[i], "%Y-%m-%d")
54+
except Exception:
55+
values[i] = None
56+
placeholders = ','.join(['%s'] * len(ORDERS_COLUMNS))
57+
sql = f"INSERT INTO orders ({', '.join(ORDERS_COLUMNS)}) VALUES ({placeholders}) ON CONFLICT (sku) DO NOTHING"
58+
try:
59+
cur.execute(sql, values)
60+
inserted += 1
61+
except Exception as e:
62+
print(f"Error inserting row {idx+1} (sku={row.get('sku')}): {e}")
63+
try:
64+
conn.commit()
65+
print("Database commit successful.")
66+
except Exception as e:
67+
print(f"Error during commit: {e}")
68+
print(f"Total rows processed: {total}")
69+
print(f"Total rows attempted to insert: {inserted}")
70+
if inserted == 0:
71+
print("No rows were inserted. Please check for errors above or review the data and schema alignment.")
72+
print("Data import attempt complete.")
73+
74+
if __name__ == "__main__":
75+
import_csv_to_orders()

0 commit comments

Comments
 (0)