Skip to content

Error Handling

jsem-nerad edited this page Nov 12, 2025 · 1 revision

Error Handling

Comprehensive guide to exception handling in strava-cz-python.

Exception Hierarchy

All library exceptions inherit from StravaAPIError:

Exception
└── StravaAPIError (base class for all Strava errors)
    ├── AuthenticationError
    ├── InsufficientBalanceError
    ├── DuplicateMealError
    └── InvalidMealTypeError

Importing Exceptions

from strava_cz import (
    StravaAPIError,           # Base exception
    AuthenticationError,       # Login/session errors
    InsufficientBalanceError, # Not enough balance
    DuplicateMealError,       # Multiple meals from same day
    InvalidMealTypeError      # Wrong meal type (e.g., soup)
)

Exception Types

1. StravaAPIError

Base exception for all API-related errors.

When raised:

  • General API failures
  • Network errors
  • Unexpected responses
  • Order/cancel verification failures

Example:

try:
    strava.menu.fetch()
except StravaAPIError as e:
    print(f"API error: {e}")

Common messages:

  • "Failed to fetch menu"
  • "Failed to save order"
  • "Failed to order meal with ID X"
  • "Failed to cancel meal with ID X"
  • "API request failed: <details>"

2. AuthenticationError

Raised for authentication and session issues.

When raised:

  • Login fails (wrong credentials)
  • Invalid canteen number
  • Already logged in
  • Not logged in when operation requires it
  • Session expired

Examples:

# Wrong credentials
try:
    strava = StravaCZ(
        username="wrong",
        password="wrong",
        canteen_number="3753"
    )
except AuthenticationError as e:
    print(f"Login failed: {e}")

# Not logged in
strava = StravaCZ()  # No login
try:
    strava.menu.fetch()
except AuthenticationError as e:
    print(f"Error: {e}")  # "User not logged in"

# Already logged in
strava = StravaCZ(username="...", password="...", canteen_number="3753")
try:
    strava.login("user", "pass", "3753")
except AuthenticationError as e:
    print(f"Error: {e}")  # "User already logged in"

Common messages:

  • "Login failed: <reason>"
  • "User not logged in"
  • "User already logged in"

3. InsufficientBalanceError

Raised when account balance is too low to order meals.

When raised:

  • Ordering meals when balance < meal cost
  • API returns error code 35

Example:

try:
    strava.menu.order_meals(3, 6, 9)  # Total: 120 Kč
except InsufficientBalanceError as e:
    print(f"Cannot order: {e}")
    print(f"Current balance: {strava.user.balance} Kč")
    
    # Calculate how much is needed
    meals = [strava.menu.get_by_id(mid) for mid in [3, 6, 9]]
    total = sum(m['price'] for m in meals if m)
    needed = total - strava.user.balance
    print(f"Need to add: {needed} Kč")

Common message:

  • API error message (usually in Czech from server)

4. DuplicateMealError

Raised when trying to order multiple meals from the same day in strict mode.

When raised:

  • Calling order_meals() with multiple IDs from same date
  • Only when strict_duplicates=True

Example:

# Meal 3 and 6 are both from 2025-11-15
try:
    strava.menu.order_meals(3, 6, strict_duplicates=True)
except DuplicateMealError as e:
    print(f"Error: {e}")
    # "Cannot order multiple meals from the same day (2025-11-15).
    #  Meal IDs 3 and 6 are from the same day."

Without strict mode (default):

# Default behavior - warns instead of error
strava.menu.order_meals(3, 6)
# Warning: Skipping meal 6 from 2025-11-15 because meal 3 from the same day is already being ordered
# Only meal 3 is ordered

5. InvalidMealTypeError

Raised when trying to order/cancel a meal type that cannot be modified.

When raised:

  • Ordering or canceling soups (MealType.SOUP)
  • Attempting to modify non-MAIN meal types

Example:

# Trying to order a soup
soup = strava.menu.get_meals(meal_types=[MealType.SOUP])[0]

try:
    strava.menu.order_meals(soup['id'])
except InvalidMealTypeError as e:
    print(f"Error: {e}")
    # "Cannot order or cancel Polévka meals. 
    #  Only main dishes (MAIN) can be ordered or canceled."

Prevention:

meal = strava.menu.get_by_id(75)

if meal['type'] == MealType.MAIN:
    strava.menu.order_meals(75)
else:
    print(f"Cannot order {meal['type'].value}")

Error Handling Patterns

Pattern 1: Specific Exception Handling

Handle each exception type differently:

from strava_cz import (
    AuthenticationError,
    InsufficientBalanceError,
    InvalidMealTypeError,
    DuplicateMealError,
    StravaAPIError
)

try:
    strava = StravaCZ(username="...", password="...", canteen_number="3753")
    strava.menu.fetch()
    strava.menu.order_meals(3, 6, strict_duplicates=True)
    
except AuthenticationError as e:
    print(f"Authentication failed: {e}")
    # Handle: prompt for credentials, exit
    
except InsufficientBalanceError as e:
    print(f"Not enough balance: {e}")
    # Handle: show balance, prompt to add funds
    
except InvalidMealTypeError as e:
    print(f"Cannot order this meal: {e}")
    # Handle: filter to only main dishes
    
except DuplicateMealError as e:
    print(f"Duplicate meals: {e}")
    # Handle: remove duplicates, retry
    
except StravaAPIError as e:
    print(f"API error: {e}")
    # Handle: retry, show error message

Pattern 2: Catch-All with Base Exception

Handle all Strava errors uniformly:

try:
    strava.menu.order_meals(3, 6, 9)
except StravaAPIError as e:
    print(f"Operation failed: {e}")
    # All Strava exceptions are caught here

Pattern 3: Graceful Degradation

Continue operation even with errors:

meal_ids = [3, 6, 9, 12, 15]
successful = []
failed = []

for meal_id in meal_ids:
    try:
        strava.menu.order_meals(meal_id)
        successful.append(meal_id)
    except StravaAPIError as e:
        failed.append((meal_id, str(e)))

print(f"Ordered: {len(successful)} meals")
print(f"Failed: {len(failed)} meals")

for meal_id, error in failed:
    print(f"  Meal {meal_id}: {error}")

Pattern 4: Retry Logic

Retry failed operations:

import time

def order_with_retry(strava, meal_ids, max_retries=3, delay=2):
    """Order meals with automatic retry."""
    for attempt in range(max_retries):
        try:
            strava.menu.order_meals(*meal_ids)
            print(f"Success on attempt {attempt + 1}")
            return True
            
        except InsufficientBalanceError:
            # Don't retry balance errors
            print("Insufficient balance - cannot retry")
            return False
            
        except StravaAPIError as e:
            if attempt < max_retries - 1:
                print(f"Attempt {attempt + 1} failed: {e}")
                print(f"Retrying in {delay} seconds...")
                time.sleep(delay)
            else:
                print(f"Failed after {max_retries} attempts")
                raise
    
    return False

# Usage
try:
    order_with_retry(strava, [3, 6, 9])
except StravaAPIError as e:
    print(f"All retries failed: {e}")

Pattern 5: Context Manager with Cleanup

Ensure proper cleanup even on errors:

class StravaSession:
    def __init__(self, username, password, canteen_number):
        self.username = username
        self.password = password
        self.canteen_number = canteen_number
        self.strava = None
    
    def __enter__(self):
        try:
            self.strava = StravaCZ(
                username=self.username,
                password=self.password,
                canteen_number=self.canteen_number
            )
            return self.strava
        except AuthenticationError as e:
            print(f"Login failed: {e}")
            raise
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.strava and self.strava.user.is_logged_in:
            try:
                self.strava.logout()
            except StravaAPIError:
                pass  # Ignore logout errors
        
        # Let the exception propagate
        return False

# Usage
try:
    with StravaSession("user", "pass", "3753") as strava:
        strava.menu.fetch()
        strava.menu.order_meals(3, 6)
except StravaAPIError as e:
    print(f"Operation failed: {e}")
# Automatically logged out

Error Context and Information

Getting Error Details

try:
    strava.menu.order_meals(3, 6, 9)
except StravaAPIError as e:
    # Error message
    print(f"Message: {e}")
    
    # Exception type
    print(f"Type: {type(e).__name__}")
    
    # String representation
    print(f"Error: {str(e)}")

Checking Current State After Error

try:
    strava.menu.order_meals(3, 6, 9)
except StravaAPIError as e:
    print(f"Error occurred: {e}")
    
    # Refresh menu to check current state
    strava.menu.fetch()
    
    # Check which meals were actually ordered
    for meal_id in [3, 6, 9]:
        is_ordered = strava.menu.is_ordered(meal_id)
        print(f"Meal {meal_id}: {'Ordered' if is_ordered else 'Not ordered'}")
    
    # Check balance
    print(f"Balance: {strava.user.balance} Kč")

Continue on Error Mode

Understanding continue_on_error

The continue_on_error parameter changes error behavior:

Default (continue_on_error=False):

  • Stops at first error
  • Rolls back all changes
  • Raises exception immediately

With continue_on_error=True):

  • Attempts all operations
  • Collects all errors
  • Raises single exception with details at end

Example with continue_on_error

# Assume meal 3 is valid, 999 doesn't exist, 6 is valid
meal_ids = [3, 999, 6]

# Without continue_on_error (default)
try:
    strava.menu.order_meals(*meal_ids)
except StravaAPIError as e:
    print(e)  # "Meal with ID 999 not found"
    # Meal 3 was NOT ordered (rolled back)
    # Meal 6 was NOT attempted

# With continue_on_error
try:
    strava.menu.order_meals(*meal_ids, continue_on_error=True)
except StravaAPIError as e:
    print(e)  # "Some meals failed to order: Meal 999: not found"
    # Meal 3 WAS ordered
    # Meal 6 WAS ordered

Validation Before Operations

Pre-validate to Avoid Errors

def validate_order(strava, meal_ids):
    """Validate meals before ordering."""
    errors = []
    
    for meal_id in meal_ids:
        meal = strava.menu.get_by_id(meal_id)
        
        if not meal:
            errors.append(f"Meal {meal_id} not found")
            continue
        
        if meal['type'] != MealType.MAIN:
            errors.append(f"Meal {meal_id} is {meal['type'].value}, not orderable")
        
        if meal['orderType'] != OrderType.NORMAL:
            errors.append(f"Meal {meal_id} is {meal['orderType'].value}")
        
        if meal['ordered']:
            errors.append(f"Meal {meal_id} already ordered")
    
    # Check balance
    meals = [strava.menu.get_by_id(mid) for mid in meal_ids]
    total = sum(m['price'] for m in meals if m)
    
    if total > strava.user.balance:
        errors.append(f"Insufficient balance: need {total}, have {strava.user.balance}")
    
    # Check duplicates
    dates = [m['date'] for m in meals if m]
    if len(dates) != len(set(dates)):
        errors.append("Multiple meals from same day")
    
    return errors

# Usage
meal_ids = [3, 6, 9]
validation_errors = validate_order(strava, meal_ids)

if validation_errors:
    print("Validation failed:")
    for error in validation_errors:
        print(f"  - {error}")
else:
    try:
        strava.menu.order_meals(*meal_ids)
    except StravaAPIError as e:
        print(f"Unexpected error: {e}")

Logging Errors

Basic Logging

import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

try:
    strava.menu.order_meals(3, 6, 9)
    logger.info("Meals ordered successfully")
except AuthenticationError as e:
    logger.error(f"Authentication failed: {e}")
except InsufficientBalanceError as e:
    logger.warning(f"Insufficient balance: {e}")
except StravaAPIError as e:
    logger.error(f"Order failed: {e}", exc_info=True)

Structured Error Logging

import json
import logging
from datetime import datetime

class StravaErrorHandler:
    def __init__(self, log_file="strava_errors.log"):
        self.log_file = log_file
    
    def log_error(self, operation, error, context=None):
        """Log error with context to file."""
        entry = {
            "timestamp": datetime.now().isoformat(),
            "operation": operation,
            "error_type": type(error).__name__,
            "error_message": str(error),
            "context": context or {}
        }
        
        with open(self.log_file, "a") as f:
            f.write(json.dumps(entry) + "\n")

# Usage
handler = StravaErrorHandler()

try:
    strava.menu.order_meals(3, 6, 9)
except StravaAPIError as e:
    handler.log_error(
        operation="order_meals",
        error=e,
        context={
            "meal_ids": [3, 6, 9],
            "balance": strava.user.balance,
            "username": strava.user.username
        }
    )

Best Practices

1. Always Handle Exceptions

# ✅ Good
try:
    strava.menu.order_meals(3, 6)
except StravaAPIError as e:
    print(f"Error: {e}")

# ❌ Bad - unhandled exception
strava.menu.order_meals(3, 6)

2. Handle Specific Exceptions First

# ✅ Good - specific to general
try:
    strava.menu.order_meals(3)
except InsufficientBalanceError as e:
    # Handle balance error
    pass
except StravaAPIError as e:
    # Handle other errors
    pass

# ❌ Bad - general exception catches everything
try:
    strava.menu.order_meals(3)
except StravaAPIError as e:
    # InsufficientBalanceError never reached
    pass
except InsufficientBalanceError as e:
    pass

3. Always Cleanup Resources

# ✅ Good
strava = None
try:
    strava = StravaCZ(...)
    # operations
finally:
    if strava and strava.user.is_logged_in:
        strava.logout()

4. Provide User-Friendly Messages

# ✅ Good
try:
    strava.menu.order_meals(3, 6)
except InsufficientBalanceError:
    print("You don't have enough money. Please add funds to your account.")
except InvalidMealTypeError:
    print("This meal cannot be ordered. Please select a main dish.")
except StravaAPIError as e:
    print(f"Something went wrong: {e}")

Next Steps

Clone this wiki locally