-
Notifications
You must be signed in to change notification settings - Fork 0
Error Handling
Every exception the library raises derives from StravaError, so one except catches
all of it:
StravaError
├── StravaAPIError .code, .status_code, .payload
│ ├── AuthenticationError
│ │ └── NotLoggedInError
│ └── InsufficientBalanceError
├── MealNotFoundError .meal_id
├── MealNotOrderableError .meal_id, .reason, .action
├── DuplicateMealError
├── MenuNotFetchedError
└── ProfileError
├── ProfileNotFoundError .name
├── KeyringUnavailableError
└── EncryptionUnavailableError
from strava_cz import StravaError
try:
strava.menu.order_meals(5)
except StravaError as exc:
print(f"Something went wrong: {exc}")The library never lets a raw httpx or json exception escape.
The API refused a request or answered with something unusable. It carries what the canteen actually said:
from strava_cz import StravaAPIError
try:
strava.menu.fetch()
except StravaAPIError as exc:
print(exc.message) # the canteen's own wording
print(exc.code) # the API's error number, or None
print(exc.status_code) # the HTTP status, or None
print(exc.payload) # the decoded body, for debuggingStrava.cz signals failure with a non-standard HTTP 555 and a body shaped
{"state": "error", "number": ..., "message": ...}. Known numbers are mapped onto
specific exceptions; anything else arrives as a StravaAPIError with code set.
Login rejected, or the session is no longer valid.
from strava_cz import AuthenticationError
try:
strava = StravaCZ("user", "wrong-password", "3753")
except AuthenticationError as exc:
print(exc.code) # 20Also raised when logging in twice on one client.
An operation that needs a session was attempted without one. A subclass of
AuthenticationError, so catching that catches this too.
from strava_cz import NotLoggedInError, StravaCZ
try:
StravaCZ().menu.fetch()
except NotLoggedInError:
print("log in first")The account cannot cover the order. The transaction is rolled back, so nothing is partly ordered.
from strava_cz import InsufficientBalanceError
try:
strava.menu.order_meals(5)
except InsufficientBalanceError as exc:
print(f"{exc.message} - you have {strava.user.balance} {strava.user.currency}")No meal with that id is in the fetched menu — usually a stale id from before the canteen republished the menu.
from strava_cz import MealNotFoundError
try:
strava.menu.order_meals(999999)
except MealNotFoundError as exc:
print(exc.meal_id)
strava.menu.fetch() # ids change with the menu; get fresh onesThe canteen does not allow the change. .reason is a
Restriction explaining why, and .action is "order" or
"cancel".
from strava_cz import MealNotOrderableError, Restriction
try:
strava.menu.order_meals(85)
except MealNotOrderableError as exc:
if exc.reason is Restriction.CLOSED:
print("too late for this day")
elif exc.reason is Restriction.UNAVAILABLE:
print("this cannot be ordered on its own")
else:
print(exc.reason.description)Checking first avoids the exception entirely:
meal = strava.menu.get_by_id(85)
if meal and meal.can_order:
strava.menu.order_meals(meal.id)Only raised with strict_duplicates=True, when two chosen meals compete for the same
slot. See Ordering Meals.
Raised by the profile store rather than by the canteen — see Profiles.
ProfileNotFoundError means no such profile (or no default is set).
KeyringUnavailableError means this machine has no usable credential store, and
EncryptionUnavailableError that cryptography is not installed to encrypt with
instead; both messages list the ways out. Plain ProfileError covers the rest: a bad
profile name, a corrupt store file, a wrong passphrase, or a password that cannot be
found anywhere.
from strava_cz import (
KeyringUnavailableError,
ProfileNotFoundError,
StravaCZ,
)
try:
strava = StravaCZ.from_profile("skola")
except ProfileNotFoundError:
print("Run: strava-cz profile add skola --username ... --canteen ...")
except KeyringUnavailableError as exc:
print(exc) # says what to use instead on a headless machineOn the command line all three exit with code 6.
The menu was used before fetch().
from strava_cz import MenuNotFetchedError
try:
days = strava.menu.get_days()
except MenuNotFetchedError:
days = strava.menu.fetch().get_days()
# or just ask
if not strava.menu.fetched:
strava.menu.fetch()Timeouts and connection failures arrive as StravaAPIError, with the underlying httpx
error kept as __cause__:
try:
strava.menu.fetch()
except StravaAPIError as exc:
print(exc) # Request to 'objednavky' timed out
print(exc.__cause__) # the original httpx exceptionEvery request carries a timeout (15s, 10s to connect by default), and connections that never got established are retried twice. Requests that reached the server are never retried, so an order cannot be placed twice by a retry.
A maintenance page or a gateway error — anything that is not JSON — also becomes a
StravaAPIError, with a snippet of the body in the message:
StravaAPIError: 'objednavky' returned 502 with a non-JSON body: '<html>Bad Gateway</html>'
By default a failed order rolls back and re-raises, so the canteen keeps the state it had. Your local menu is refreshed as part of that, so you can simply look again:
from strava_cz import StravaError
try:
strava.menu.order_meals(5, 9)
except StravaError as exc:
print(f"Nothing was ordered: {exc}")
for meal in strava.menu.get_meals(orderable=True):
print(f" still available: [{meal.id}] {meal.name}")To get partial success instead of an exception, use continue_on_error=True and read
the OrderResult.
Skipped duplicates are reported through the warnings module rather than an exception:
import warnings
with warnings.catch_warnings():
warnings.simplefilter("error") # turn them into exceptions
strava.menu.order_meals(9, 10)import os
import sys
from strava_cz import (
AuthenticationError,
InsufficientBalanceError,
MealType,
StravaCZ,
StravaError,
)
try:
with StravaCZ(
os.environ["STRAVA_USERNAME"],
os.environ["STRAVA_PASSWORD"],
os.environ["STRAVA_CANTEEN"],
) as strava:
strava.menu.fetch()
wanted = [
day.orderable[0].id
for day in strava.menu.get_days()
if day.orderable and not day.ordered
]
result = strava.menu.order_meals(*wanted, continue_on_error=True)
print(f"Ordered {len(result.changed)} meals, {len(result.failed)} failed")
for meal_id, error in result.failed:
print(f" {meal_id}: {error}", file=sys.stderr)
except AuthenticationError:
sys.exit("Check your credentials")
except InsufficientBalanceError as exc:
sys.exit(f"Top up your account: {exc.message}")
except StravaError as exc:
sys.exit(f"Strava.cz problem: {exc}")