rename grocery to product
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
146
backend/main.py
146
backend/main.py
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user