diff --git a/backend/TEST_DATA_README.md b/backend/TEST_DATA_README.md new file mode 100644 index 0000000..38a7a23 --- /dev/null +++ b/backend/TEST_DATA_README.md @@ -0,0 +1,222 @@ +# Test Data Scripts Documentation + +This directory contains scripts for creating and managing test data for the Grocery Tracker application. + +## Scripts Overview + +### 1. `create_test_data.py` - Comprehensive Test Data Generator + +Creates realistic test data including shops, groceries, and shopping events. + +#### Basic Usage + +```bash +# Create all test data (default: 30 events over 90 days) +python create_test_data.py + +# Create with custom parameters +python create_test_data.py --events 50 --days 120 + +# Verbose output +python create_test_data.py --verbose + +# Dry run (see what would be created without creating it) +python create_test_data.py --dry-run +``` + +#### Command Line Options + +| Option | Description | Default | +|--------|-------------|---------| +| `--events N` | Number of shopping events to create | 30 | +| `--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 | +| `--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 | + +#### Examples + +```bash +# Create only shops +python create_test_data.py --shops-only + +# Create only groceries +python create_test_data.py --groceries-only + +# Create 100 shopping events using existing shops and groceries +python create_test_data.py --events-only --events 100 + +# Create test data for the past 6 months with verbose output +python create_test_data.py --events 60 --days 180 --verbose + +# Preview what would be created without actually creating it +python create_test_data.py --dry-run + +# Use a different API URL +python create_test_data.py --url http://localhost:3000 +``` + +### 2. `cleanup_test_data.py` - Data Cleanup Script + +Safely removes all test data with confirmation prompts. + +```bash +python cleanup_test_data.py +``` + +## Data Structure + +### Shops (10 total) +- **Whole Foods Market** (San Francisco) +- **Safeway** (San Francisco) +- **Trader Joe's** (Berkeley) +- **Berkeley Bowl** (Berkeley) +- **Rainbow Grocery** (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) + +| Category | Items | Organic Options | +|----------|-------|-----------------| +| **Fruits** | 10 items | 5 organic | +| **Vegetables** | 10 items | 5 organic | +| **Dairy** | 7 items | 4 organic | +| **Meat & Seafood** | 6 items | 3 organic | +| **Pantry** | 10 items | 5 organic | +| **Beverages** | 6 items | 3 organic | +| **Frozen** | 5 items | 2 organic | +| **Snacks** | 5 items | 3 organic | + +### Shopping Events +- **Realistic dates**: Distributed over specified time period (default: 90 days) +- **Smart quantities**: Appropriate amounts based on item type +- **Category-based pricing**: Realistic price ranges per category +- **Organic premiums**: 20-50% higher prices for organic items +- **Random notes**: 30% of events include descriptive notes +- **Varied trip sizes**: 2-8 items per shopping trip + +## Features + +### Smart Data Generation +- **Realistic pricing**: Category-based price ranges with organic premiums +- **Appropriate quantities**: Items sold by piece, weight, or volume as appropriate +- **Temporal distribution**: Events spread realistically over time +- **Shopping patterns**: Varied trip sizes and frequencies + +### Error Handling +- **Graceful failures**: Script continues even if some items fail +- **Network timeouts**: Reasonable timeout values for API calls +- **Progress tracking**: Clear feedback on creation progress +- **Connection testing**: Verifies API availability before starting + +### Flexible Options +- **Partial creation**: Create only specific data types +- **Custom parameters**: Adjust event count and date range +- **Dry run mode**: Preview without creating data +- **Verbose output**: Detailed progress information +- **Custom API URLs**: Support for different backend configurations + +## Troubleshooting + +### Common Issues + +1. **Connection Error** + ``` + โŒ Cannot connect to the API server at http://localhost:8000 + ``` + **Solution**: Make sure the backend server is running: + ```bash + cd backend + uvicorn main:app --reload + ``` + +2. **Module Not Found Error** + ``` + ModuleNotFoundError: No module named 'requests' + ``` + **Solution**: Install dependencies: + ```bash + pip install -r requirements.txt + ``` + +3. **Database Connection Error** + **Solution**: Ensure PostgreSQL is running and database exists: + ```bash + # Check if PostgreSQL is running + brew services list | grep postgresql + + # Start PostgreSQL if needed + brew services start postgresql + ``` + +4. **Partial Data Creation** + If some items fail to create, the script will continue and report the final count. + Use `--verbose` to see detailed error messages. + +### Performance Tips + +- **Large datasets**: For creating many events (100+), consider running in smaller batches +- **Network issues**: Use `--verbose` to identify specific failures +- **Database performance**: Ensure your database has adequate resources for bulk operations + +## Data Cleanup + +To remove all test data: + +```bash +python cleanup_test_data.py +``` + +This script will: +1. Show what data exists +2. Ask for confirmation before deletion +3. Handle foreign key constraints properly +4. Provide progress feedback + +## Integration with Application + +After running the test data scripts: + +1. **Frontend**: Refresh the application to see new data +2. **API**: All endpoints will return the test data +3. **Database**: Data is persisted and will survive server restarts + +The test data is designed to showcase all application features: +- Multiple shops and locations +- Diverse grocery categories +- Realistic shopping patterns +- Price variations and organic options +- Historical data for analytics + +## Advanced Usage + +### Custom Data Sets + +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 +- Adjust price ranges for different markets +- Create seasonal shopping patterns + +### Automated Testing + +The scripts can be integrated into automated testing workflows: + +```bash +# Setup test environment +python create_test_data.py --events 10 --days 30 + +# Run tests +pytest + +# Cleanup +python cleanup_test_data.py --force # (if you add a --force option) +``` \ No newline at end of file diff --git a/backend/cleanup_test_data.py b/backend/cleanup_test_data.py new file mode 100644 index 0000000..e3ceb53 --- /dev/null +++ b/backend/cleanup_test_data.py @@ -0,0 +1,200 @@ +#!/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. +""" + +import requests +import json +from typing import List, Dict, Any + +BASE_URL = "http://localhost:8000" + +def delete_all_shopping_events() -> int: + """Delete all shopping events and return the count of deleted events.""" + print("๐Ÿ›’ Deleting all shopping events...") + + try: + # Get all shopping events + response = requests.get(f"{BASE_URL}/shopping-events/") + if response.status_code != 200: + print(f" โŒ Failed to fetch shopping events: {response.status_code}") + return 0 + + events = response.json() + deleted_count = 0 + + for event in events: + try: + delete_response = requests.delete(f"{BASE_URL}/shopping-events/{event['id']}") + if delete_response.status_code == 200: + deleted_count += 1 + print(f" โœ… Deleted event #{event['id']} from {event['shop']['name']}") + else: + print(f" โŒ Failed to delete event #{event['id']}: {delete_response.status_code}") + except Exception as e: + print(f" โŒ Error deleting event #{event['id']}: {e}") + + print(f" ๐Ÿ“Š Deleted {deleted_count} shopping events total\n") + return deleted_count + + except Exception as e: + 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...") + + try: + # Get all groceries + response = requests.get(f"{BASE_URL}/groceries/") + if response.status_code != 200: + print(f" โŒ Failed to fetch groceries: {response.status_code}") + return 0 + + groceries = response.json() + deleted_count = 0 + + for grocery in groceries: + try: + delete_response = requests.delete(f"{BASE_URL}/groceries/{grocery['id']}") + if delete_response.status_code == 200: + deleted_count += 1 + organic_label = "๐ŸŒฑ" if grocery['organic'] else "๐ŸŒพ" + print(f" โœ… Deleted grocery: {organic_label} {grocery['name']}") + else: + print(f" โŒ Failed to delete grocery {grocery['name']}: {delete_response.status_code}") + except Exception as e: + print(f" โŒ Error deleting grocery {grocery['name']}: {e}") + + print(f" ๐Ÿ“Š Deleted {deleted_count} groceries total\n") + return deleted_count + + except Exception as e: + print(f" โŒ Error fetching groceries: {e}") + return 0 + +def delete_all_shops() -> int: + """Delete all shops and return the count of deleted shops.""" + print("๐Ÿช Deleting all shops...") + + try: + # Get all shops + response = requests.get(f"{BASE_URL}/shops/") + if response.status_code != 200: + print(f" โŒ Failed to fetch shops: {response.status_code}") + return 0 + + shops = response.json() + deleted_count = 0 + + for shop in shops: + try: + delete_response = requests.delete(f"{BASE_URL}/shops/{shop['id']}") + if delete_response.status_code == 200: + deleted_count += 1 + print(f" โœ… Deleted shop: {shop['name']} ({shop['city']})") + else: + print(f" โŒ Failed to delete shop {shop['name']}: {delete_response.status_code}") + except Exception as e: + print(f" โŒ Error deleting shop {shop['name']}: {e}") + + print(f" ๐Ÿ“Š Deleted {deleted_count} shops total\n") + return deleted_count + + except Exception as e: + print(f" โŒ Error fetching shops: {e}") + return 0 + +def get_current_data_summary(): + """Get a summary of current data in the database.""" + print("๐Ÿ“Š CURRENT DATA SUMMARY") + print("=" * 30) + + try: + # Get counts + shops_response = requests.get(f"{BASE_URL}/shops/") + groceries_response = requests.get(f"{BASE_URL}/groceries/") + 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 + 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"๐Ÿ›’ Shopping Events: {events_count}") + + if events_count > 0 and events_response.status_code == 200: + events = events_response.json() + total_spent = sum(event.get('total_amount', 0) for event in events) + print(f"๐Ÿ’ฐ Total spent: ${total_spent:.2f}") + + print() + return shops_count, groceries_count, events_count + + except Exception as e: + print(f"โŒ Error getting data summary: {e}") + return 0, 0, 0 + +def main(): + """Main function to clean up all test data.""" + print("๐Ÿงน GROCERY TRACKER DATA CLEANUP") + print("=" * 40) + print("This script will delete ALL data from the Grocery Tracker app.") + print("Make sure the backend server is running on http://localhost:8000\n") + + try: + # Test connection + response = requests.get(f"{BASE_URL}/") + if response.status_code != 200: + print("โŒ Cannot connect to the API server. Make sure it's running!") + return + + print("โœ… Connected to API server\n") + + # Show current data + shops_count, groceries_count, events_count = get_current_data_summary() + + if shops_count == 0 and groceries_count == 0 and events_count == 0: + print("โœ… Database is already empty. Nothing to clean up!") + return + + # Confirm deletion + print("โš ๏ธ WARNING: This will permanently delete all data!") + confirmation = input("Type 'DELETE' to confirm: ") + + if confirmation != 'DELETE': + print("โŒ Cleanup cancelled.") + return + + print("\n๐Ÿงน Starting cleanup process...\n") + + # Delete in order: events -> groceries -> shops + # (due to foreign key constraints) + deleted_events = delete_all_shopping_events() + deleted_groceries = delete_all_groceries() + 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"๐Ÿช Shops deleted: {deleted_shops}") + print(f"๐Ÿ“Š Total items deleted: {deleted_events + deleted_groceries + deleted_shops}") + + print("\n๐ŸŽ‰ Cleanup completed successfully!") + print("The database is now empty and ready for fresh data.") + + except requests.exceptions.ConnectionError: + print("โŒ Could not connect to the API server.") + print(" Make sure the backend server is running on http://localhost:8000") + except KeyboardInterrupt: + print("\nโŒ Cleanup cancelled by user.") + except Exception as e: + print(f"โŒ Unexpected error: {e}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/backend/create_test_data.py b/backend/create_test_data.py new file mode 100644 index 0000000..8f5962d --- /dev/null +++ b/backend/create_test_data.py @@ -0,0 +1,459 @@ +#!/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. +""" + +import requests +import json +import random +import argparse +import sys +from datetime import datetime, timedelta +from typing import List, Dict, Any + +BASE_URL = "http://localhost:8000" + +# Test data definitions +SHOPS_DATA = [ + {"name": "Whole Foods Market", "city": "San Francisco", "address": "1765 California St"}, + {"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": "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"}, + {"name": "Sprouts Farmers Market", "city": "Sunnyvale", "address": "1077 E El Camino Real"}, + {"name": "Lucky Supermarket", "city": "San Jose", "address": "1717 Tully Rd"}, +] + +GROCERIES_DATA = [ + # Fruits + {"name": "Organic Bananas", "category": "Fruits", "organic": True, "weight": 1.0, "weight_unit": "lb"}, + {"name": "Gala Apples", "category": "Fruits", "organic": False, "weight": 2.0, "weight_unit": "lb"}, + {"name": "Organic Strawberries", "category": "Fruits", "organic": True, "weight": 1.0, "weight_unit": "lb"}, + {"name": "Avocados", "category": "Fruits", "organic": False, "weight": None, "weight_unit": "piece"}, + {"name": "Organic Blueberries", "category": "Fruits", "organic": True, "weight": 0.5, "weight_unit": "lb"}, + {"name": "Lemons", "category": "Fruits", "organic": False, "weight": None, "weight_unit": "piece"}, + {"name": "Organic Oranges", "category": "Fruits", "organic": True, "weight": 3.0, "weight_unit": "lb"}, + {"name": "Grapes", "category": "Fruits", "organic": False, "weight": 2.0, "weight_unit": "lb"}, + {"name": "Organic Pears", "category": "Fruits", "organic": True, "weight": 2.0, "weight_unit": "lb"}, + {"name": "Pineapple", "category": "Fruits", "organic": False, "weight": None, "weight_unit": "piece"}, + + # Vegetables + {"name": "Organic Spinach", "category": "Vegetables", "organic": True, "weight": 5.0, "weight_unit": "oz"}, + {"name": "Carrots", "category": "Vegetables", "organic": False, "weight": 2.0, "weight_unit": "lb"}, + {"name": "Organic Broccoli", "category": "Vegetables", "organic": True, "weight": None, "weight_unit": "piece"}, + {"name": "Red Bell Peppers", "category": "Vegetables", "organic": False, "weight": None, "weight_unit": "piece"}, + {"name": "Organic Kale", "category": "Vegetables", "organic": True, "weight": 1.0, "weight_unit": "bunch"}, + {"name": "Tomatoes", "category": "Vegetables", "organic": False, "weight": 2.0, "weight_unit": "lb"}, + {"name": "Organic Sweet Potatoes", "category": "Vegetables", "organic": True, "weight": 3.0, "weight_unit": "lb"}, + {"name": "Cucumbers", "category": "Vegetables", "organic": False, "weight": None, "weight_unit": "piece"}, + {"name": "Organic Lettuce", "category": "Vegetables", "organic": True, "weight": None, "weight_unit": "head"}, + {"name": "Onions", "category": "Vegetables", "organic": False, "weight": 3.0, "weight_unit": "lb"}, + + # Dairy + {"name": "Organic Whole Milk", "category": "Dairy", "organic": True, "weight": 1.0, "weight_unit": "gallon"}, + {"name": "Greek Yogurt", "category": "Dairy", "organic": False, "weight": 32.0, "weight_unit": "oz"}, + {"name": "Organic Eggs", "category": "Dairy", "organic": True, "weight": None, "weight_unit": "dozen"}, + {"name": "Cheddar Cheese", "category": "Dairy", "organic": False, "weight": 8.0, "weight_unit": "oz"}, + {"name": "Organic Butter", "category": "Dairy", "organic": True, "weight": 1.0, "weight_unit": "lb"}, + {"name": "Cream Cheese", "category": "Dairy", "organic": False, "weight": 8.0, "weight_unit": "oz"}, + {"name": "Organic Yogurt", "category": "Dairy", "organic": True, "weight": 6.0, "weight_unit": "oz"}, + + # Meat & Seafood + {"name": "Organic Chicken Breast", "category": "Meat & Seafood", "organic": True, "weight": 2.0, "weight_unit": "lb"}, + {"name": "Ground Beef", "category": "Meat & Seafood", "organic": False, "weight": 1.0, "weight_unit": "lb"}, + {"name": "Wild Salmon Fillet", "category": "Meat & Seafood", "organic": False, "weight": 1.5, "weight_unit": "lb"}, + {"name": "Organic Ground Turkey", "category": "Meat & Seafood", "organic": True, "weight": 1.0, "weight_unit": "lb"}, + {"name": "Shrimp", "category": "Meat & Seafood", "organic": False, "weight": 1.0, "weight_unit": "lb"}, + {"name": "Organic Chicken Thighs", "category": "Meat & Seafood", "organic": True, "weight": 2.0, "weight_unit": "lb"}, + + # Pantry + {"name": "Organic Brown Rice", "category": "Pantry", "organic": True, "weight": 2.0, "weight_unit": "lb"}, + {"name": "Whole Wheat Bread", "category": "Pantry", "organic": False, "weight": 24.0, "weight_unit": "oz"}, + {"name": "Organic Quinoa", "category": "Pantry", "organic": True, "weight": 1.0, "weight_unit": "lb"}, + {"name": "Olive Oil", "category": "Pantry", "organic": False, "weight": 500.0, "weight_unit": "ml"}, + {"name": "Organic Pasta", "category": "Pantry", "organic": True, "weight": 1.0, "weight_unit": "lb"}, + {"name": "Black Beans", "category": "Pantry", "organic": False, "weight": 15.0, "weight_unit": "oz"}, + {"name": "Organic Oats", "category": "Pantry", "organic": True, "weight": 18.0, "weight_unit": "oz"}, + {"name": "Peanut Butter", "category": "Pantry", "organic": False, "weight": 18.0, "weight_unit": "oz"}, + {"name": "Organic Honey", "category": "Pantry", "organic": True, "weight": 12.0, "weight_unit": "oz"}, + {"name": "Canned Tomatoes", "category": "Pantry", "organic": False, "weight": 14.5, "weight_unit": "oz"}, + + # Beverages + {"name": "Organic Orange Juice", "category": "Beverages", "organic": True, "weight": 64.0, "weight_unit": "oz"}, + {"name": "Sparkling Water", "category": "Beverages", "organic": False, "weight": 1.0, "weight_unit": "l"}, + {"name": "Organic Green Tea", "category": "Beverages", "organic": True, "weight": None, "weight_unit": "box"}, + {"name": "Coffee Beans", "category": "Beverages", "organic": False, "weight": 12.0, "weight_unit": "oz"}, + {"name": "Almond Milk", "category": "Beverages", "organic": False, "weight": 32.0, "weight_unit": "oz"}, + {"name": "Organic Apple Juice", "category": "Beverages", "organic": True, "weight": 64.0, "weight_unit": "oz"}, + + # Frozen + {"name": "Organic Frozen Berries", "category": "Frozen", "organic": True, "weight": 10.0, "weight_unit": "oz"}, + {"name": "Frozen Pizza", "category": "Frozen", "organic": False, "weight": 12.0, "weight_unit": "oz"}, + {"name": "Organic Frozen Vegetables", "category": "Frozen", "organic": True, "weight": 16.0, "weight_unit": "oz"}, + {"name": "Ice Cream", "category": "Frozen", "organic": False, "weight": 48.0, "weight_unit": "oz"}, + {"name": "Frozen Fish Fillets", "category": "Frozen", "organic": False, "weight": 1.0, "weight_unit": "lb"}, + + # Snacks + {"name": "Organic Granola Bars", "category": "Snacks", "organic": True, "weight": 8.0, "weight_unit": "oz"}, + {"name": "Potato Chips", "category": "Snacks", "organic": False, "weight": 5.0, "weight_unit": "oz"}, + {"name": "Organic Nuts", "category": "Snacks", "organic": True, "weight": 6.0, "weight_unit": "oz"}, + {"name": "Crackers", "category": "Snacks", "organic": False, "weight": 7.0, "weight_unit": "oz"}, + {"name": "Organic Popcorn", "category": "Snacks", "organic": True, "weight": 3.0, "weight_unit": "oz"}, +] + +# Price ranges for different categories (min, max) +PRICE_RANGES = { + "Fruits": (1.99, 8.99), + "Vegetables": (0.99, 6.99), + "Dairy": (2.49, 12.99), + "Meat & Seafood": (4.99, 24.99), + "Pantry": (1.99, 15.99), + "Beverages": (1.99, 8.99), + "Frozen": (2.99, 9.99), + "Snacks": (1.49, 7.99), +} + +def parse_arguments(): + """Parse command line arguments.""" + parser = argparse.ArgumentParser(description='Create test data for Grocery 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('--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() + +def check_api_connection(base_url: str) -> bool: + """Check if the API server is accessible.""" + try: + response = requests.get(f"{base_url}/", timeout=5) + return response.status_code == 200 + except requests.exceptions.RequestException: + return False + +def create_shops(base_url: str, verbose: bool = False, dry_run: bool = False) -> List[Dict[str, Any]]: + """Create shops and return the created shop objects.""" + print("๐Ÿช Creating shops...") + created_shops = [] + + if dry_run: + print(" [DRY RUN] Would create the following shops:") + for shop_data in SHOPS_DATA: + print(f" ๐Ÿ“‹ {shop_data['name']} in {shop_data['city']}") + return [] + + for shop_data in SHOPS_DATA: + try: + if verbose: + print(f" ๐Ÿ”„ Creating shop: {shop_data['name']}...") + + response = requests.post(f"{base_url}/shops/", json=shop_data, timeout=10) + if response.status_code == 200: + shop = response.json() + created_shops.append(shop) + print(f" โœ… Created shop: {shop['name']} in {shop['city']}") + else: + print(f" โŒ Failed to create shop {shop_data['name']}: {response.status_code}") + if verbose: + print(f" Response: {response.text}") + except requests.exceptions.RequestException as e: + print(f" โŒ Network error creating shop {shop_data['name']}: {e}") + except Exception as e: + print(f" โŒ Error creating shop {shop_data['name']}: {e}") + + 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 = [] + + 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']})") + return [] + + for grocery_data in GROCERIES_DATA: + try: + if verbose: + print(f" ๐Ÿ”„ Creating grocery: {grocery_data['name']}...") + + response = requests.post(f"{base_url}/groceries/", json=grocery_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']})") + else: + print(f" โŒ Failed to create grocery {grocery_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}") + except Exception as e: + print(f" โŒ Error creating grocery {grocery_data['name']}: {e}") + + print(f" ๐Ÿ“Š Created {len(created_groceries)} groceries total\n") + return created_groceries + +def generate_random_price(category: str, organic: bool = False) -> float: + """Generate a random price for a grocery 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 + if organic: + min_price *= 1.2 + max_price *= 1.5 + + # Generate random price and round to nearest cent + price = random.uniform(min_price, max_price) + return round(price, 2) + +def get_existing_data(base_url: str) -> tuple[List[Dict], List[Dict]]: + """Get existing shops and groceries from the API.""" + try: + shops_response = requests.get(f"{base_url}/shops/", timeout=10) + groceries_response = requests.get(f"{base_url}/groceries/", timeout=10) + + shops = shops_response.json() if shops_response.status_code == 200 else [] + groceries = groceries_response.json() if groceries_response.status_code == 200 else [] + + return shops, groceries + 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, + 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.""" + print(f"๐Ÿ›’ Creating {num_events} shopping events...") + created_events = [] + + if not shops: + print(" โŒ No shops available. Cannot create shopping events.") + return [] + + if not groceries: + print(" โŒ No groceries available. Cannot create shopping events.") + return [] + + # Generate events over the specified time period + end_date = datetime.now() + start_date = end_date - timedelta(days=days_back) + + if dry_run: + 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)}") + return [] + + for i in range(num_events): + try: + if verbose: + print(f" ๐Ÿ”„ Creating shopping event {i+1}/{num_events}...") + + # Random shop and date + shop = random.choice(shops) + event_date = start_date + timedelta( + days=random.randint(0, days_back), + hours=random.randint(8, 20), + 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))) + + # Create grocery items for this event + event_groceries = [] + total_amount = 0.0 + + for grocery in selected_groceries: + # Random amount based on item type + if grocery['weight_unit'] == 'piece': + amount = random.randint(1, 4) + elif grocery['weight_unit'] == 'dozen': + amount = 1 + elif grocery['weight_unit'] in ['box', 'head', 'bunch']: + amount = random.randint(1, 2) + elif grocery['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']) + + event_groceries.append({ + "grocery_id": grocery['id'], + "amount": amount, + "price": price + }) + + total_amount += amount * price + + # Round total amount + total_amount = round(total_amount, 2) + + # Random notes (30% chance of having notes) + notes = None + if random.random() < 0.3: + note_options = [ + "Weekly grocery shopping", + "Quick lunch ingredients", + "Dinner party prep", + "Meal prep for the week", + "Emergency grocery run", + "Organic produce haul", + "Bulk shopping trip", + "Special occasion shopping", + "Holiday meal preparation", + "Healthy eating restart", + "Stocking up on essentials", + "Trying new recipes", + ] + notes = random.choice(note_options) + + # Create the shopping event + event_data = { + "shop_id": shop['id'], + "date": event_date.isoformat(), + "total_amount": total_amount, + "notes": notes, + "groceries": event_groceries + } + + 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)") + else: + print(f" โŒ Failed to create shopping event: {response.status_code}") + if verbose: + print(f" Response: {response.text}") + except requests.exceptions.RequestException as e: + print(f" โŒ Network error creating shopping event {i+1}: {e}") + except Exception as e: + print(f" โŒ Error creating shopping event {i+1}: {e}") + + 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]): + """Print a summary of the created test data.""" + print("๐Ÿ“‹ TEST DATA SUMMARY") + print("=" * 50) + + print(f"๐Ÿช Shops: {len(shops)}") + for shop in shops: + print(f" โ€ข {shop['name']} ({shop['city']})") + + print(f"\n๐Ÿฅฌ Groceries: {len(groceries)}") + categories = {} + for grocery in groceries: + category = grocery['category'] + if category not in categories: + categories[category] = [] + categories[category].append(grocery) + + for category, items in categories.items(): + organic_count = sum(1 for item in items if item['organic']) + print(f" โ€ข {category}: {len(items)} items ({organic_count} organic)") + + print(f"\n๐Ÿ›’ Shopping Events: {len(events)}") + if events: + total_spent = sum(event.get('total_amount', 0) for event in events) + avg_spent = total_spent / len(events) if events else 0 + print(f" โ€ข Total spent: ${total_spent:.2f}") + print(f" โ€ข Average per trip: ${avg_spent:.2f}") + + # Shop distribution + shop_counts = {} + for event in events: + shop_name = event['shop']['name'] + shop_counts[shop_name] = shop_counts.get(shop_name, 0) + 1 + + print(" โ€ข Events per shop:") + for shop_name, count in sorted(shop_counts.items(), key=lambda x: x[1], reverse=True): + print(f" - {shop_name}: {count} events") + +def main(): + """Main function to create all test data.""" + args = parse_arguments() + + print("๐Ÿš€ GROCERY TRACKER TEST DATA GENERATOR") + print("=" * 50) + + if args.dry_run: + print("๐Ÿ” DRY RUN MODE - No data will be created") + + print(f"API URL: {args.url}") + print(f"Shopping events: {args.events}") + print(f"Date range: {args.days} days back") + print() + + try: + # Test connection + if not check_api_connection(args.url): + print(f"โŒ Cannot connect to the API server at {args.url}") + print(" Make sure the backend server is running!") + sys.exit(1) + + print("โœ… Connected to API server\n") + + shops = [] + groceries = [] + 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.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) + 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) + + # Print summary + if not args.dry_run: + print_summary(shops, groceries, events) + + if args.dry_run: + print("\n๐Ÿ” Dry run completed. Use without --dry-run to actually create the data.") + else: + print("\n๐ŸŽ‰ Test data creation completed successfully!") + print("You can now explore the application with realistic data.") + + except KeyboardInterrupt: + print("\nโŒ Operation cancelled by user.") + sys.exit(1) + except requests.exceptions.ConnectionError: + print(f"โŒ Could not connect to the API server at {args.url}") + print(" Make sure the backend server is running!") + sys.exit(1) + except Exception as e: + print(f"โŒ Unexpected error: {e}") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index eeca44c..157feb0 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -10,4 +10,5 @@ python-multipart>=0.0.6 python-dotenv>=1.0.0 pytest>=7.4.3 pytest-asyncio>=0.21.1 -httpx>=0.25.2 \ No newline at end of file +httpx>=0.25.2 +requests>=2.31.0 \ No newline at end of file