-
Notifications
You must be signed in to change notification settings - Fork 0
Error Handling
Comprehensive guide to exception handling in strava-cz-python.
All library exceptions inherit from StravaAPIError:
Exception
└── StravaAPIError (base class for all Strava errors)
├── AuthenticationError
├── InsufficientBalanceError
├── DuplicateMealError
└── InvalidMealTypeError
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)
)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>"
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"
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)
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 orderedRaised 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}")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 messageHandle 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 hereContinue 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}")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}")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 outtry:
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)}")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č")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
# 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 ordereddef 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}")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)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
}
)# ✅ 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)# ✅ 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# ✅ Good
strava = None
try:
strava = StravaCZ(...)
# operations
finally:
if strava and strava.user.is_logged_in:
strava.logout()# ✅ 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}")- Learn about Balance Management
- Read the Exceptions API Reference
- Explore Menu Class methods