Skip to content

Commit 621aff2

Browse files
Merge pull request #81 from goldlabelapps/staging
This pull request introduces a new "orders" feature to the API, including a new endpoint for paginated order retrieval, scripts for managing the `orders` table and importing data, and updates to the API documentation and root endpoints.
2 parents a9e2946 + c713e85 commit 621aff2

11 files changed

Lines changed: 4572 additions & 7 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ This project provides a scalable API backend using FastAPI and PostgreSQL, featu
2626
### 1. Clone & Setup Environment
2727

2828
```bash
29-
git clone <repo-url>
29+
git clone https://github.com/goldlabelapps/python.git
3030
cd python
3131
cp .env.sample .env # Add your Postgres credentials and settings
3232
python -m venv venv

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: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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+
12+
13+
# Refactored GET /orders endpoint to return paginated, filtered, and ordered results
14+
@router.get("/orders")
15+
def get_orders(
16+
page: int = Query(1, ge=1, description="Page number (1-based)"),
17+
limit: int = Query(100, ge=1, le=500, description="Records per page (default 100, max 500)"),
18+
search: str = Query(None, description="Search string (case-insensitive, partial match)"),
19+
hideflagged: bool = Query(False, description="If true, flagged records are excluded")
20+
) -> dict:
21+
"""Return paginated, filtered, and ordered records, filtered by search if provided."""
22+
meta = make_meta("success", "Read paginated orders")
23+
conn_gen = get_db_connection()
24+
conn = next(conn_gen)
25+
cur = conn.cursor()
26+
offset = (page - 1) * limit
27+
try:
28+
# Build WHERE clause
29+
where_clauses = ["hide IS NOT TRUE"]
30+
params = []
31+
if hideflagged:
32+
where_clauses.append("flag IS NOT TRUE")
33+
# No first_name/last_name search, as those columns do not exist
34+
where_sql = " AND ".join(where_clauses)
35+
36+
# Count query
37+
count_query = f'SELECT COUNT(*) FROM orders WHERE {where_sql};'
38+
cur.execute(count_query, params)
39+
count_row = cur.fetchone() if cur.description is not None else None
40+
total = count_row[0] if count_row is not None else 0
41+
42+
# Data query
43+
data_query = f'''
44+
SELECT * FROM orders
45+
WHERE {where_sql}
46+
OFFSET %s LIMIT %s;
47+
'''
48+
cur.execute(data_query, params + [offset, limit])
49+
if cur.description is not None:
50+
columns = [desc[0] for desc in cur.description]
51+
rows = cur.fetchall()
52+
data = [dict(zip(columns, row)) for row in rows]
53+
else:
54+
data = []
55+
except Exception as e:
56+
data = []
57+
total = 0
58+
meta = make_meta("error", f"Failed to read orders: {str(e)}")
59+
finally:
60+
cur.close()
61+
conn.close()
62+
return {
63+
"meta": meta,
64+
"pagination": {
65+
"page": page,
66+
"limit": limit,
67+
"total": total,
68+
"pages": (total // limit) + (1 if total % limit else 0)
69+
},
70+
"data": data,
71+
}
72+
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: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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+
print("Clearing orders table before import...")
23+
cur.execute("TRUNCATE TABLE orders;")
24+
print("orders table cleared.")
25+
seen_skus = set()
26+
with open(CSV_PATH, newline='', encoding='utf-8') as csvfile:
27+
reader = csv.DictReader(csvfile)
28+
for idx, row in enumerate(reader):
29+
original_sku = row.get("sku")
30+
new_sku = original_sku
31+
# Ensure SKU is unique in this import batch
32+
suffix = 1
33+
while new_sku in seen_skus:
34+
new_sku = f"{original_sku}_{suffix}"
35+
suffix += 1
36+
if new_sku != original_sku:
37+
print(f"Duplicate SKU found: {original_sku}, changed to {new_sku}")
38+
row["sku"] = new_sku
39+
seen_skus.add(new_sku)
40+
total += 1
41+
# Add hide and flag fields if not present
42+
row.setdefault("hide", False)
43+
row.setdefault("flag", False)
44+
# Convert empty strings to None
45+
values = [row.get(col) if row.get(col) != '' else None for col in ORDERS_COLUMNS]
46+
# Print first row and values for debug
47+
if idx == 0:
48+
print("First CSV row:", row)
49+
print("First values list:", values)
50+
# Convert booleans and numerics as needed
51+
for i, col in enumerate(ORDERS_COLUMNS):
52+
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"]:
53+
if values[i] is not None:
54+
values[i] = str(values[i]).lower() in ("1", "true", "t", "yes")
55+
elif col in ["weight", "price", "special_price", "map_price", "msrp_price", "qty", "out_of_stock_qty", "min_cart_qty", "max_cart_qty", "qty_increments"]:
56+
if values[i] is not None:
57+
try:
58+
values[i] = float(values[i])
59+
except ValueError:
60+
values[i] = None
61+
elif col.endswith("_date") or col.endswith("_at") or col in ["custom_design_from", "custom_design_to"]:
62+
if values[i] is not None:
63+
try:
64+
# Try parsing as date or datetime
65+
values[i] = datetime.strptime(values[i], "%m/%d/%y")
66+
except Exception:
67+
try:
68+
values[i] = datetime.strptime(values[i], "%Y-%m-%d")
69+
except Exception:
70+
values[i] = None
71+
placeholders = ','.join(['%s'] * len(ORDERS_COLUMNS))
72+
sql = f"INSERT INTO orders ({', '.join(ORDERS_COLUMNS)}) VALUES ({placeholders})"
73+
try:
74+
cur.execute(sql, values)
75+
inserted += 1
76+
except Exception as e:
77+
print(f"Error inserting row {idx+1} (sku={row.get('sku')}): {e}")
78+
try:
79+
conn.commit()
80+
print("Database commit successful.")
81+
except Exception as e:
82+
print(f"Error during commit: {e}")
83+
print(f"Total rows processed: {total}")
84+
print(f"Total rows attempted to insert: {inserted}")
85+
if inserted == 0:
86+
print("No rows were inserted. Please check for errors above or review the data and schema alignment.")
87+
print("Data import attempt complete.")
88+
89+
if __name__ == "__main__":
90+
import_csv_to_orders()

0 commit comments

Comments
 (0)