rename grocery to product

This commit is contained in:
2025-05-26 20:20:21 +02:00
parent 1b984d18d9
commit d27871160e
26 changed files with 1114 additions and 498 deletions

View File

@@ -1,12 +1,12 @@
# Test Data Scripts Documentation
This directory contains scripts for creating and managing test data for the Grocery Tracker application.
This directory contains scripts for creating and managing test data for the Product Tracker application.
## Scripts Overview
### 1. `create_test_data.py` - Comprehensive Test Data Generator
Creates realistic test data including shops, groceries, and shopping events.
Creates realistic test data including shops, products, and shopping events.
#### Basic Usage
@@ -32,7 +32,7 @@ python create_test_data.py --dry-run
| `--days N` | Number of days back to generate events | 90 |
| `--url URL` | API base URL | http://localhost:8000 |
| `--shops-only` | Create only shops | False |
| `--groceries-only` | Create only groceries | False |
| `--products-only` | Create only products | False |
| `--events-only` | Create only shopping events (requires existing data) | False |
| `--verbose`, `-v` | Verbose output with detailed progress | False |
| `--dry-run` | Show what would be created without creating it | False |
@@ -43,10 +43,10 @@ python create_test_data.py --dry-run
# Create only shops
python create_test_data.py --shops-only
# Create only groceries
python create_test_data.py --groceries-only
# Create only products
python create_test_data.py --products-only
# Create 100 shopping events using existing shops and groceries
# Create 100 shopping events using existing shops and products
python create_test_data.py --events-only --events 100
# Create test data for the past 6 months with verbose output
@@ -74,14 +74,14 @@ python cleanup_test_data.py
- **Safeway** (San Francisco)
- **Trader Joe's** (Berkeley)
- **Berkeley Bowl** (Berkeley)
- **Rainbow Grocery** (San Francisco)
- **Rainbow Product** (San Francisco)
- **Mollie Stone's Market** (Palo Alto)
- **Costco Wholesale** (San Mateo)
- **Target** (Mountain View)
- **Sprouts Farmers Market** (Sunnyvale)
- **Lucky Supermarket** (San Jose)
### Groceries (50+ items across 8 categories)
### Products (50+ items across 8 categories)
| Category | Items | Organic Options |
|----------|-------|-----------------|
@@ -190,7 +190,7 @@ After running the test data scripts:
The test data is designed to showcase all application features:
- Multiple shops and locations
- Diverse grocery categories
- Diverse product categories
- Realistic shopping patterns
- Price variations and organic options
- Historical data for analytics
@@ -202,7 +202,7 @@ The test data is designed to showcase all application features:
You can modify the data arrays in `create_test_data.py` to create custom test scenarios:
- Add more shops for specific regions
- Include specialty grocery categories
- Include specialty product categories
- Adjust price ranges for different markets
- Create seasonal shopping patterns

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""
Script to clean up all test data from the Grocery Tracker application.
This will delete all shopping events, groceries, and shops.
Script to clean up all test data from the Product Tracker application.
This will delete all shopping events, products, and shops.
"""
import requests
@@ -42,37 +42,37 @@ def delete_all_shopping_events() -> int:
print(f" ❌ Error fetching shopping events: {e}")
return 0
def delete_all_groceries() -> int:
"""Delete all groceries and return the count of deleted groceries."""
print("🥬 Deleting all groceries...")
def delete_all_products() -> int:
"""Delete all products and return the count of deleted products."""
print("🥬 Deleting all products...")
try:
# Get all groceries
response = requests.get(f"{BASE_URL}/groceries/")
# Get all products
response = requests.get(f"{BASE_URL}/products/")
if response.status_code != 200:
print(f" ❌ Failed to fetch groceries: {response.status_code}")
print(f" ❌ Failed to fetch products: {response.status_code}")
return 0
groceries = response.json()
products = response.json()
deleted_count = 0
for grocery in groceries:
for product in products:
try:
delete_response = requests.delete(f"{BASE_URL}/groceries/{grocery['id']}")
delete_response = requests.delete(f"{BASE_URL}/products/{product['id']}")
if delete_response.status_code == 200:
deleted_count += 1
organic_label = "🌱" if grocery['organic'] else "🌾"
print(f" ✅ Deleted grocery: {organic_label} {grocery['name']}")
organic_label = "🌱" if product['organic'] else "🌾"
print(f" ✅ Deleted product: {organic_label} {product['name']}")
else:
print(f" ❌ Failed to delete grocery {grocery['name']}: {delete_response.status_code}")
print(f" ❌ Failed to delete product {product['name']}: {delete_response.status_code}")
except Exception as e:
print(f" ❌ Error deleting grocery {grocery['name']}: {e}")
print(f" ❌ Error deleting product {product['name']}: {e}")
print(f" 📊 Deleted {deleted_count} groceries total\n")
print(f" 📊 Deleted {deleted_count} products total\n")
return deleted_count
except Exception as e:
print(f" ❌ Error fetching groceries: {e}")
print(f" ❌ Error fetching products: {e}")
return 0
def delete_all_shops() -> int:
@@ -115,15 +115,15 @@ def get_current_data_summary():
try:
# Get counts
shops_response = requests.get(f"{BASE_URL}/shops/")
groceries_response = requests.get(f"{BASE_URL}/groceries/")
products_response = requests.get(f"{BASE_URL}/products/")
events_response = requests.get(f"{BASE_URL}/shopping-events/")
shops_count = len(shops_response.json()) if shops_response.status_code == 200 else 0
groceries_count = len(groceries_response.json()) if groceries_response.status_code == 200 else 0
products_count = len(products_response.json()) if products_response.status_code == 200 else 0
events_count = len(events_response.json()) if events_response.status_code == 200 else 0
print(f"🏪 Shops: {shops_count}")
print(f"🥬 Groceries: {groceries_count}")
print(f"🥬 Products: {products_count}")
print(f"🛒 Shopping Events: {events_count}")
if events_count > 0 and events_response.status_code == 200:
@@ -132,7 +132,7 @@ def get_current_data_summary():
print(f"💰 Total spent: ${total_spent:.2f}")
print()
return shops_count, groceries_count, events_count
return shops_count, products_count, events_count
except Exception as e:
print(f"❌ Error getting data summary: {e}")
@@ -140,9 +140,9 @@ def get_current_data_summary():
def main():
"""Main function to clean up all test data."""
print("🧹 GROCERY TRACKER DATA CLEANUP")
print("🧹 PRODUCT TRACKER DATA CLEANUP")
print("=" * 40)
print("This script will delete ALL data from the Grocery Tracker app.")
print("This script will delete ALL data from the Product Tracker app.")
print("Make sure the backend server is running on http://localhost:8000\n")
try:
@@ -155,9 +155,9 @@ def main():
print("✅ Connected to API server\n")
# Show current data
shops_count, groceries_count, events_count = get_current_data_summary()
shops_count, products_count, events_count = get_current_data_summary()
if shops_count == 0 and groceries_count == 0 and events_count == 0:
if shops_count == 0 and products_count == 0 and events_count == 0:
print("✅ Database is already empty. Nothing to clean up!")
return
@@ -171,19 +171,19 @@ def main():
print("\n🧹 Starting cleanup process...\n")
# Delete in order: events -> groceries -> shops
# Delete in order: events -> products -> shops
# (due to foreign key constraints)
deleted_events = delete_all_shopping_events()
deleted_groceries = delete_all_groceries()
deleted_products = delete_all_products()
deleted_shops = delete_all_shops()
# Final summary
print("📋 CLEANUP SUMMARY")
print("=" * 30)
print(f"🛒 Shopping Events deleted: {deleted_events}")
print(f"🥬 Groceries deleted: {deleted_groceries}")
print(f"🥬 Products deleted: {deleted_products}")
print(f"🏪 Shops deleted: {deleted_shops}")
print(f"📊 Total items deleted: {deleted_events + deleted_groceries + deleted_shops}")
print(f"📊 Total items deleted: {deleted_events + deleted_products + deleted_shops}")
print("\n🎉 Cleanup completed successfully!")
print("The database is now empty and ready for fresh data.")

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""
Script to create comprehensive test data for the Grocery Tracker application.
This includes shops, groceries, and shopping events with realistic data.
Script to create comprehensive test data for the Product Tracker application.
This includes shops, products, and shopping events with realistic data.
"""
import requests
@@ -20,7 +20,7 @@ SHOPS_DATA = [
{"name": "Safeway", "city": "San Francisco", "address": "2020 Market St"},
{"name": "Trader Joe's", "city": "Berkeley", "address": "1885 University Ave"},
{"name": "Berkeley Bowl", "city": "Berkeley", "address": "2020 Oregon St"},
{"name": "Rainbow Grocery", "city": "San Francisco", "address": "1745 Folsom St"},
{"name": "Rainbow Product", "city": "San Francisco", "address": "1745 Folsom St"},
{"name": "Mollie Stone's Market", "city": "Palo Alto", "address": "164 S California Ave"},
{"name": "Costco Wholesale", "city": "San Mateo", "address": "2300 S Norfolk St"},
{"name": "Target", "city": "Mountain View", "address": "1200 El Camino Real"},
@@ -119,13 +119,13 @@ PRICE_RANGES = {
def parse_arguments():
"""Parse command line arguments."""
parser = argparse.ArgumentParser(description='Create test data for Grocery Tracker')
parser = argparse.ArgumentParser(description='Create test data for Product Tracker')
parser.add_argument('--events', type=int, default=30, help='Number of shopping events to create (default: 30)')
parser.add_argument('--days', type=int, default=90, help='Number of days back to generate events (default: 90)')
parser.add_argument('--url', type=str, default=BASE_URL, help='API base URL (default: http://localhost:8000)')
parser.add_argument('--shops-only', action='store_true', help='Create only shops')
parser.add_argument('--groceries-only', action='store_true', help='Create only groceries')
parser.add_argument('--events-only', action='store_true', help='Create only shopping events (requires existing shops and groceries)')
parser.add_argument('--products-only', action='store_true', help='Create only products')
parser.add_argument('--events-only', action='store_true', help='Create only shopping events (requires existing shops and products)')
parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output')
parser.add_argument('--dry-run', action='store_true', help='Show what would be created without actually creating it')
return parser.parse_args()
@@ -171,43 +171,43 @@ def create_shops(base_url: str, verbose: bool = False, dry_run: bool = False) ->
print(f" 📊 Created {len(created_shops)} shops total\n")
return created_shops
def create_groceries(base_url: str, verbose: bool = False, dry_run: bool = False) -> List[Dict[str, Any]]:
"""Create groceries and return the created grocery objects."""
print("🥬 Creating groceries...")
created_groceries = []
def create_products(base_url: str, verbose: bool = False, dry_run: bool = False) -> List[Dict[str, Any]]:
"""Create products and return the created product objects."""
print("🥬 Creating products...")
created_products = []
if dry_run:
print(" [DRY RUN] Would create the following groceries:")
for grocery_data in GROCERIES_DATA:
organic_label = "🌱" if grocery_data['organic'] else "🌾"
print(f" 📋 {organic_label} {grocery_data['name']} ({grocery_data['category']})")
print(" [DRY RUN] Would create the following products:")
for product_data in GROCERIES_DATA:
organic_label = "🌱" if product_data['organic'] else "🌾"
print(f" 📋 {organic_label} {product_data['name']} ({product_data['category']})")
return []
for grocery_data in GROCERIES_DATA:
for product_data in GROCERIES_DATA:
try:
if verbose:
print(f" 🔄 Creating grocery: {grocery_data['name']}...")
print(f" 🔄 Creating product: {product_data['name']}...")
response = requests.post(f"{base_url}/groceries/", json=grocery_data, timeout=10)
response = requests.post(f"{base_url}/products/", json=product_data, timeout=10)
if response.status_code == 200:
grocery = response.json()
created_groceries.append(grocery)
organic_label = "🌱" if grocery['organic'] else "🌾"
print(f" ✅ Created grocery: {organic_label} {grocery['name']} ({grocery['category']})")
product = response.json()
created_products.append(product)
organic_label = "🌱" if product['organic'] else "🌾"
print(f" ✅ Created product: {organic_label} {product['name']} ({product['category']})")
else:
print(f" ❌ Failed to create grocery {grocery_data['name']}: {response.status_code}")
print(f" ❌ Failed to create product {product_data['name']}: {response.status_code}")
if verbose:
print(f" Response: {response.text}")
except requests.exceptions.RequestException as e:
print(f" ❌ Network error creating grocery {grocery_data['name']}: {e}")
print(f" ❌ Network error creating product {product_data['name']}: {e}")
except Exception as e:
print(f" ❌ Error creating grocery {grocery_data['name']}: {e}")
print(f" ❌ Error creating product {product_data['name']}: {e}")
print(f" 📊 Created {len(created_groceries)} groceries total\n")
return created_groceries
print(f" 📊 Created {len(created_products)} products total\n")
return created_products
def generate_random_price(category: str, organic: bool = False) -> float:
"""Generate a random price for a grocery item based on category and organic status."""
"""Generate a random price for a product item based on category and organic status."""
min_price, max_price = PRICE_RANGES.get(category, (1.99, 9.99))
# Organic items are typically 20-50% more expensive
@@ -220,23 +220,23 @@ def generate_random_price(category: str, organic: bool = False) -> float:
return round(price, 2)
def get_existing_data(base_url: str) -> tuple[List[Dict], List[Dict]]:
"""Get existing shops and groceries from the API."""
"""Get existing shops and products from the API."""
try:
shops_response = requests.get(f"{base_url}/shops/", timeout=10)
groceries_response = requests.get(f"{base_url}/groceries/", timeout=10)
products_response = requests.get(f"{base_url}/products/", timeout=10)
shops = shops_response.json() if shops_response.status_code == 200 else []
groceries = groceries_response.json() if groceries_response.status_code == 200 else []
products = products_response.json() if products_response.status_code == 200 else []
return shops, groceries
return shops, products
except requests.exceptions.RequestException as e:
print(f" ❌ Error fetching existing data: {e}")
return [], []
def create_shopping_events(shops: List[Dict], groceries: List[Dict], base_url: str,
def create_shopping_events(shops: List[Dict], products: List[Dict], base_url: str,
num_events: int = 25, days_back: int = 90,
verbose: bool = False, dry_run: bool = False) -> List[Dict[str, Any]]:
"""Create shopping events with random groceries and realistic data."""
"""Create shopping events with random products and realistic data."""
print(f"🛒 Creating {num_events} shopping events...")
created_events = []
@@ -244,8 +244,8 @@ def create_shopping_events(shops: List[Dict], groceries: List[Dict], base_url: s
print(" ❌ No shops available. Cannot create shopping events.")
return []
if not groceries:
print(" ❌ No groceries available. Cannot create shopping events.")
if not products:
print(" ❌ No products available. Cannot create shopping events.")
return []
# Generate events over the specified time period
@@ -256,7 +256,7 @@ def create_shopping_events(shops: List[Dict], groceries: List[Dict], base_url: s
print(f" [DRY RUN] Would create {num_events} shopping events over {days_back} days")
print(f" 📋 Date range: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}")
print(f" 📋 Available shops: {len(shops)}")
print(f" 📋 Available groceries: {len(groceries)}")
print(f" 📋 Available products: {len(products)}")
return []
for i in range(num_events):
@@ -272,32 +272,32 @@ def create_shopping_events(shops: List[Dict], groceries: List[Dict], base_url: s
minutes=random.randint(0, 59)
)
# Random number of groceries (2-8 items per shopping trip)
num_groceries = random.randint(2, 8)
selected_groceries = random.sample(groceries, min(num_groceries, len(groceries)))
# Random number of products (2-8 items per shopping trip)
num_products = random.randint(2, 8)
selected_products = random.sample(products, min(num_products, len(products)))
# Create grocery items for this event
event_groceries = []
# Create product items for this event
event_products = []
total_amount = 0.0
for grocery in selected_groceries:
for product in selected_products:
# Random amount based on item type
if grocery['weight_unit'] == 'piece':
if product['weight_unit'] == 'piece':
amount = random.randint(1, 4)
elif grocery['weight_unit'] == 'dozen':
elif product['weight_unit'] == 'dozen':
amount = 1
elif grocery['weight_unit'] in ['box', 'head', 'bunch']:
elif product['weight_unit'] in ['box', 'head', 'bunch']:
amount = random.randint(1, 2)
elif grocery['weight_unit'] in ['gallon', 'l']:
elif product['weight_unit'] in ['gallon', 'l']:
amount = 1
else:
amount = round(random.uniform(0.5, 3.0), 2)
# Generate price based on category and organic status
price = generate_random_price(grocery['category'], grocery['organic'])
price = generate_random_price(product['category'], product['organic'])
event_groceries.append({
"grocery_id": grocery['id'],
event_products.append({
"product_id": product['id'],
"amount": amount,
"price": price
})
@@ -311,11 +311,11 @@ def create_shopping_events(shops: List[Dict], groceries: List[Dict], base_url: s
notes = None
if random.random() < 0.3:
note_options = [
"Weekly grocery shopping",
"Weekly product shopping",
"Quick lunch ingredients",
"Dinner party prep",
"Meal prep for the week",
"Emergency grocery run",
"Emergency product run",
"Organic produce haul",
"Bulk shopping trip",
"Special occasion shopping",
@@ -332,14 +332,14 @@ def create_shopping_events(shops: List[Dict], groceries: List[Dict], base_url: s
"date": event_date.isoformat(),
"total_amount": total_amount,
"notes": notes,
"groceries": event_groceries
"products": event_products
}
response = requests.post(f"{base_url}/shopping-events/", json=event_data, timeout=15)
if response.status_code == 200:
event = response.json()
created_events.append(event)
print(f" ✅ Created event #{event['id']}: {shop['name']} - ${total_amount:.2f} ({len(event_groceries)} items)")
print(f" ✅ Created event #{event['id']}: {shop['name']} - ${total_amount:.2f} ({len(event_products)} items)")
else:
print(f" ❌ Failed to create shopping event: {response.status_code}")
if verbose:
@@ -352,7 +352,7 @@ def create_shopping_events(shops: List[Dict], groceries: List[Dict], base_url: s
print(f" 📊 Created {len(created_events)} shopping events total\n")
return created_events
def print_summary(shops: List[Dict], groceries: List[Dict], events: List[Dict]):
def print_summary(shops: List[Dict], products: List[Dict], events: List[Dict]):
"""Print a summary of the created test data."""
print("📋 TEST DATA SUMMARY")
print("=" * 50)
@@ -361,13 +361,13 @@ def print_summary(shops: List[Dict], groceries: List[Dict], events: List[Dict]):
for shop in shops:
print(f"{shop['name']} ({shop['city']})")
print(f"\n🥬 Groceries: {len(groceries)}")
print(f"\n🥬 Products: {len(products)}")
categories = {}
for grocery in groceries:
category = grocery['category']
for product in products:
category = product['category']
if category not in categories:
categories[category] = []
categories[category].append(grocery)
categories[category].append(product)
for category, items in categories.items():
organic_count = sum(1 for item in items if item['organic'])
@@ -394,7 +394,7 @@ def main():
"""Main function to create all test data."""
args = parse_arguments()
print("🚀 GROCERY TRACKER TEST DATA GENERATOR")
print("🚀 PRODUCT TRACKER TEST DATA GENERATOR")
print("=" * 50)
if args.dry_run:
@@ -415,28 +415,28 @@ def main():
print("✅ Connected to API server\n")
shops = []
groceries = []
products = []
events = []
# Create data based on arguments
if args.shops_only:
shops = create_shops(args.url, args.verbose, args.dry_run)
elif args.groceries_only:
groceries = create_groceries(args.url, args.verbose, args.dry_run)
elif args.products_only:
products = create_products(args.url, args.verbose, args.dry_run)
elif args.events_only:
# Get existing data for events
shops, groceries = get_existing_data(args.url)
events = create_shopping_events(shops, groceries, args.url, args.events, args.days, args.verbose, args.dry_run)
shops, products = get_existing_data(args.url)
events = create_shopping_events(shops, products, args.url, args.events, args.days, args.verbose, args.dry_run)
else:
# Create all data
shops = create_shops(args.url, args.verbose, args.dry_run)
groceries = create_groceries(args.url, args.verbose, args.dry_run)
if shops and groceries:
events = create_shopping_events(shops, groceries, args.url, args.events, args.days, args.verbose, args.dry_run)
products = create_products(args.url, args.verbose, args.dry_run)
if shops and products:
events = create_shopping_events(shops, products, args.url, args.events, args.days, args.verbose, args.dry_run)
# Print summary
if not args.dry_run:
print_summary(shops, groceries, events)
print_summary(shops, products, events)
if args.dry_run:
print("\n🔍 Dry run completed. Use without --dry-run to actually create the data.")

View File

@@ -11,7 +11,7 @@ DATABASE_URL = os.getenv("DATABASE_URL")
if not DATABASE_URL:
# Default to SQLite for development if no PostgreSQL URL is provided
DATABASE_URL = "sqlite:///./grocery_tracker.db"
DATABASE_URL = "sqlite:///./product_tracker.db"
print("🔄 Using SQLite database for development")
else:
print(f"🐘 Using PostgreSQL database")

View File

@@ -1,9 +1,9 @@
# Database Configuration
# Option 1: PostgreSQL (for production)
# DATABASE_URL=postgresql://username:password@localhost:5432/grocery_tracker
# DATABASE_URL=postgresql://username:password@localhost:5432/product_tracker
# Option 2: SQLite (for development - default if DATABASE_URL is not set)
# DATABASE_URL=sqlite:///./grocery_tracker.db
# DATABASE_URL=sqlite:///./product_tracker.db
# Authentication (optional for basic setup)
SECRET_KEY=your-secret-key-here

View File

@@ -10,8 +10,8 @@ from database import engine, get_db
models.Base.metadata.create_all(bind=engine)
app = FastAPI(
title="Grocery Tracker API",
description="API for tracking grocery prices and shopping events",
title="Product Tracker API",
description="API for tracking product prices and shopping events",
version="1.0.0"
)
@@ -25,22 +25,22 @@ app.add_middleware(
)
def build_shopping_event_response(event: models.ShoppingEvent, db: Session) -> schemas.ShoppingEventResponse:
"""Build a shopping event response with groceries from the association table"""
# Get groceries with their event-specific data
grocery_data = db.execute(
"""Build a shopping event response with products from the association table"""
# Get products with their event-specific data
product_data = db.execute(
text("""
SELECT g.id, g.name, g.category, g.organic, g.weight, g.weight_unit,
seg.amount, seg.price
FROM groceries g
JOIN shopping_event_groceries seg ON g.id = seg.grocery_id
WHERE seg.shopping_event_id = :event_id
SELECT p.id, p.name, p.category, p.organic, p.weight, p.weight_unit,
sep.amount, sep.price
FROM products p
JOIN shopping_event_products sep ON p.id = sep.product_id
WHERE sep.shopping_event_id = :event_id
"""),
{"event_id": event.id}
).fetchall()
# Convert to GroceryWithEventData objects
groceries_with_data = [
schemas.GroceryWithEventData(
# Convert to ProductWithEventData objects
products_with_data = [
schemas.ProductWithEventData(
id=row.id,
name=row.name,
category=row.category,
@@ -50,7 +50,7 @@ def build_shopping_event_response(event: models.ShoppingEvent, db: Session) -> s
amount=row.amount,
price=row.price
)
for row in grocery_data
for row in product_data
]
return schemas.ShoppingEventResponse(
@@ -61,58 +61,58 @@ def build_shopping_event_response(event: models.ShoppingEvent, db: Session) -> s
notes=event.notes,
created_at=event.created_at,
shop=event.shop,
groceries=groceries_with_data
products=products_with_data
)
# Root endpoint
@app.get("/")
def read_root():
return {"message": "Grocery Tracker API", "version": "1.0.0"}
return {"message": "Product Tracker API", "version": "1.0.0"}
# Grocery endpoints
@app.post("/groceries/", response_model=schemas.Grocery)
def create_grocery(grocery: schemas.GroceryCreate, db: Session = Depends(get_db)):
db_grocery = models.Grocery(**grocery.dict())
db.add(db_grocery)
# Product endpoints
@app.post("/products/", response_model=schemas.Product)
def create_product(product: schemas.ProductCreate, db: Session = Depends(get_db)):
db_product = models.Product(**product.dict())
db.add(db_product)
db.commit()
db.refresh(db_grocery)
return db_grocery
db.refresh(db_product)
return db_product
@app.get("/groceries/", response_model=List[schemas.Grocery])
def read_groceries(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
groceries = db.query(models.Grocery).offset(skip).limit(limit).all()
return groceries
@app.get("/products/", response_model=List[schemas.Product])
def read_products(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
products = db.query(models.Product).offset(skip).limit(limit).all()
return products
@app.get("/groceries/{grocery_id}", response_model=schemas.Grocery)
def read_grocery(grocery_id: int, db: Session = Depends(get_db)):
grocery = db.query(models.Grocery).filter(models.Grocery.id == grocery_id).first()
if grocery is None:
raise HTTPException(status_code=404, detail="Grocery not found")
return grocery
@app.get("/products/{product_id}", response_model=schemas.Product)
def read_product(product_id: int, db: Session = Depends(get_db)):
product = db.query(models.Product).filter(models.Product.id == product_id).first()
if product is None:
raise HTTPException(status_code=404, detail="Product not found")
return product
@app.put("/groceries/{grocery_id}", response_model=schemas.Grocery)
def update_grocery(grocery_id: int, grocery_update: schemas.GroceryUpdate, db: Session = Depends(get_db)):
grocery = db.query(models.Grocery).filter(models.Grocery.id == grocery_id).first()
if grocery is None:
raise HTTPException(status_code=404, detail="Grocery not found")
@app.put("/products/{product_id}", response_model=schemas.Product)
def update_product(product_id: int, product_update: schemas.ProductUpdate, db: Session = Depends(get_db)):
product = db.query(models.Product).filter(models.Product.id == product_id).first()
if product is None:
raise HTTPException(status_code=404, detail="Product not found")
update_data = grocery_update.dict(exclude_unset=True)
update_data = product_update.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(grocery, field, value)
setattr(product, field, value)
db.commit()
db.refresh(grocery)
return grocery
db.refresh(product)
return product
@app.delete("/groceries/{grocery_id}")
def delete_grocery(grocery_id: int, db: Session = Depends(get_db)):
grocery = db.query(models.Grocery).filter(models.Grocery.id == grocery_id).first()
if grocery is None:
raise HTTPException(status_code=404, detail="Grocery not found")
@app.delete("/products/{product_id}")
def delete_product(product_id: int, db: Session = Depends(get_db)):
product = db.query(models.Product).filter(models.Product.id == product_id).first()
if product is None:
raise HTTPException(status_code=404, detail="Product not found")
db.delete(grocery)
db.delete(product)
db.commit()
return {"message": "Grocery deleted successfully"}
return {"message": "Product deleted successfully"}
# Shop endpoints
@app.post("/shops/", response_model=schemas.Shop)
@@ -178,19 +178,19 @@ def create_shopping_event(event: schemas.ShoppingEventCreate, db: Session = Depe
db.commit()
db.refresh(db_event)
# Add groceries to the event
for grocery_item in event.groceries:
grocery = db.query(models.Grocery).filter(models.Grocery.id == grocery_item.grocery_id).first()
if grocery is None:
raise HTTPException(status_code=404, detail=f"Grocery with id {grocery_item.grocery_id} not found")
# Add products to the event
for product_item in event.products:
product = db.query(models.Product).filter(models.Product.id == product_item.product_id).first()
if product is None:
raise HTTPException(status_code=404, detail=f"Product with id {product_item.product_id} not found")
# Insert into association table
db.execute(
models.shopping_event_groceries.insert().values(
models.shopping_event_products.insert().values(
shopping_event_id=db_event.id,
grocery_id=grocery_item.grocery_id,
amount=grocery_item.amount,
price=grocery_item.price
product_id=product_item.product_id,
amount=product_item.amount,
price=product_item.price
)
)
@@ -228,26 +228,26 @@ def update_shopping_event(event_id: int, event_update: schemas.ShoppingEventCrea
event.total_amount = event_update.total_amount
event.notes = event_update.notes
# Remove existing grocery associations
# Remove existing product associations
db.execute(
models.shopping_event_groceries.delete().where(
models.shopping_event_groceries.c.shopping_event_id == event_id
models.shopping_event_products.delete().where(
models.shopping_event_products.c.shopping_event_id == event_id
)
)
# Add new grocery associations
for grocery_item in event_update.groceries:
grocery = db.query(models.Grocery).filter(models.Grocery.id == grocery_item.grocery_id).first()
if grocery is None:
raise HTTPException(status_code=404, detail=f"Grocery with id {grocery_item.grocery_id} not found")
# Add new product associations
for product_item in event_update.products:
product = db.query(models.Product).filter(models.Product.id == product_item.product_id).first()
if product is None:
raise HTTPException(status_code=404, detail=f"Product with id {product_item.product_id} not found")
# Insert into association table
db.execute(
models.shopping_event_groceries.insert().values(
models.shopping_event_products.insert().values(
shopping_event_id=event_id,
grocery_id=grocery_item.grocery_id,
amount=grocery_item.amount,
price=grocery_item.price
product_id=product_item.product_id,
amount=product_item.amount,
price=product_item.price
)
)
@@ -261,10 +261,10 @@ def delete_shopping_event(event_id: int, db: Session = Depends(get_db)):
if event is None:
raise HTTPException(status_code=404, detail="Shopping event not found")
# Delete grocery associations first
# Delete product associations first
db.execute(
models.shopping_event_groceries.delete().where(
models.shopping_event_groceries.c.shopping_event_id == event_id
models.shopping_event_products.delete().where(
models.shopping_event_products.c.shopping_event_id == event_id
)
)

View File

@@ -6,19 +6,19 @@ from datetime import datetime
Base = declarative_base()
# Association table for many-to-many relationship between shopping events and groceries
shopping_event_groceries = Table(
'shopping_event_groceries',
# Association table for many-to-many relationship between shopping events and products
shopping_event_products = Table(
'shopping_event_products',
Base.metadata,
Column('id', Integer, primary_key=True, autoincrement=True), # Artificial primary key
Column('shopping_event_id', Integer, ForeignKey('shopping_events.id'), nullable=False),
Column('grocery_id', Integer, ForeignKey('groceries.id'), nullable=False),
Column('amount', Float, nullable=False), # Amount of this grocery bought in this event
Column('price', Float, nullable=False) # Price of this grocery at the time of this shopping event
Column('product_id', Integer, ForeignKey('products.id'), nullable=False),
Column('amount', Float, nullable=False), # Amount of this product bought in this event
Column('price', Float, nullable=False) # Price of this product at the time of this shopping event
)
class Grocery(Base):
__tablename__ = "groceries"
class Product(Base):
__tablename__ = "products"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, nullable=False, index=True)
@@ -30,7 +30,7 @@ class Grocery(Base):
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
shopping_events = relationship("ShoppingEvent", secondary=shopping_event_groceries, back_populates="groceries")
shopping_events = relationship("ShoppingEvent", secondary=shopping_event_products, back_populates="products")
class Shop(Base):
__tablename__ = "shops"
@@ -58,4 +58,4 @@ class ShoppingEvent(Base):
# Relationships
shop = relationship("Shop", back_populates="shopping_events")
groceries = relationship("Grocery", secondary=shopping_event_groceries, back_populates="shopping_events")
products = relationship("Product", secondary=shopping_event_products, back_populates="shopping_events")

View File

@@ -22,7 +22,7 @@ def main():
backend_dir = Path(__file__).parent
os.chdir(backend_dir)
print("🍃 Starting Grocery Tracker Backend Development Server")
print("🍃 Starting Product Tracker Backend Development Server")
print("=" * 50)
# Check if virtual environment exists

View File

@@ -3,24 +3,24 @@ from typing import Optional, List
from datetime import datetime
# Base schemas
class GroceryBase(BaseModel):
class ProductBase(BaseModel):
name: str
category: str
organic: bool = False
weight: Optional[float] = None
weight_unit: str = "g"
class GroceryCreate(GroceryBase):
class ProductCreate(ProductBase):
pass
class GroceryUpdate(BaseModel):
class ProductUpdate(BaseModel):
name: Optional[str] = None
category: Optional[str] = None
organic: Optional[bool] = None
weight: Optional[float] = None
weight_unit: Optional[str] = None
class Grocery(GroceryBase):
class Product(ProductBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
@@ -51,12 +51,12 @@ class Shop(ShopBase):
from_attributes = True
# Shopping Event schemas
class GroceryInEvent(BaseModel):
grocery_id: int
class ProductInEvent(BaseModel):
product_id: int
amount: float = Field(..., gt=0)
price: float = Field(..., ge=0) # Price at the time of this shopping event (allow free items)
class GroceryWithEventData(BaseModel):
class ProductWithEventData(BaseModel):
id: int
name: str
category: str
@@ -76,21 +76,21 @@ class ShoppingEventBase(BaseModel):
notes: Optional[str] = None
class ShoppingEventCreate(ShoppingEventBase):
groceries: List[GroceryInEvent] = []
products: List[ProductInEvent] = []
class ShoppingEventUpdate(BaseModel):
shop_id: Optional[int] = None
date: Optional[datetime] = None
total_amount: Optional[float] = Field(None, ge=0)
notes: Optional[str] = None
groceries: Optional[List[GroceryInEvent]] = None
products: Optional[List[ProductInEvent]] = None
class ShoppingEventResponse(ShoppingEventBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
shop: Shop
groceries: List[GroceryWithEventData] = []
products: List[ProductWithEventData] = []
class Config:
from_attributes = True