Strava.cz Python API
Python knihovna pro objednávání jídel ve školních jídelnách, které používají systém Strava.cz. Komunikuje přímo s interním JSON API webové aplikace, takže nepotřebuje prohlížeč ani parsování HTML.
pip install strava-cz # knihovna
pip install "strava-cz[cli]" # + bezpečné uložení hesla pro příkaz strava-cz
Ve složce notes najdete všechny moje poznatky o vnitřním fungování API Strava.cz. Kompletní dokumentace je na GitHub Wiki.
Co knihovna umí
- Přihlášení a odhlášení, včetně správy session
- Načtení a filtrování jídelníčku podle typu jídla, data a objednatelnosti
- Objednávání a rušení jídel v jedné transakci, s ověřením výsledku
- Rozpoznání, proč konkrétní jídlo objednat nelze (uzávěrka, prázdniny, ...)
- Sledování zůstatku na účtu
- Rozlišení dnů, na které jídelna neobjednává automaticky
- Příkaz
strava-czpro práci z terminálu, s výpisem pro lidi i s JSON pro skripty - Profily, takže se přihlašovací údaje nezadávají znovu a heslo leží v klíčence
- Plné typové anotace (
py.typed), takže vám editor napovídá
Rychlý start
from strava_cz import StravaCZ
with StravaCZ("vase.jmeno", "VaseHeslo123", "3753") as strava:
print(strava.user)
strava.menu.fetch()
strava.menu.print()
# Objedná oběd s ID 5 a 9
strava.menu.order_meals(5, 9)
with blok se sám postará o odhlášení a uzavření spojení. Pokud ho nepoužijete,
zavolejte na konci strava.logout() sami.
ID jídla je unikátní číslo jídla v rámci právě publikovaného jídelníčku (v API pole
veta). Není trvale vázané na konkrétní jídlo a mění se s každou změnou jídelníčku, takže ho vždy berte z čerstvě načteného jídelníčku.
Práce s jídelníčkem
from strava_cz import StravaCZ, MealType
strava = StravaCZ("vase.jmeno", "VaseHeslo123", "3753")
strava.menu.fetch()
# Dny a jídla
dny = strava.menu.get_days() # seznam objektů Day
jidla = strava.menu.get_meals() # plochý seznam objektů Meal
# Filtrování - všechny parametry jdou kombinovat
obedy = strava.menu.get_meals(meal_types=[MealType.MAIN], orderable=True)
tento_tyden = strava.menu.get_days(date_from="2026-09-07", date_to="2026-09-11")
objednane = strava.menu.get_meals(ordered=True)
# Vyhledávání
den = strava.menu.get_by_date("2026-09-07")
jidlo = strava.menu.get_by_id(9)
print(strava.menu.is_ordered(9))
# Iterace, len() a indexování fungují přímo na menu
for den in strava.menu:
print(den.date, [j.name for j in den.meals])
Ve výchozím nastavení se nic neskrývá — vrátí se každé publikované jídlo
a u každého se dozvíte, jestli jde objednat. Jedinou výjimkou jsou dny, kdy se
nevaří; ty se přidají pomocí include_no_school=True.
Objekt Meal
| atribut | typ | popis |
|---|---|---|
id |
int |
Identifikátor pro objednávání (API pole veta) |
date |
datetime.date |
Den, kdy se jídlo vydává |
type |
MealType |
SOUP, MAIN nebo UNKNOWN |
variant |
str |
Označení, které jídlu dává jídelna, například "Oběd 1" |
name |
str |
Název jídla |
price |
float |
Cena |
ordered |
bool |
Jestli je jídlo objednané |
can_order |
bool |
Jestli ho lze právě teď objednat |
can_cancel |
bool |
Jestli lze objednávku zrušit |
order_restriction |
Restriction |
Proč objednat nelze |
cancel_restriction |
Restriction |
Proč zrušit nelze |
allergens |
tuple[Allergen, ...] |
Alergeny, dvojice (kód, název) |
deadline |
datetime | None |
Dokdy lze jídlo objednat |
raw |
dict |
Nezpracovaný záznam z API |
Objekt Day
| atribut | typ | popis |
|---|---|---|
date |
datetime.date |
Datum |
meals |
tuple[Meal, ...] |
Jídla toho dne |
ordered |
bool |
Jestli je aspoň jedno jídlo objednané |
orderable |
tuple[Meal, ...] |
Jídla, která lze objednat |
no_school |
bool |
Jestli se ten den nevaří |
auto_ordered |
bool |
Jestli na ten den jídelna objednává automaticky |
status |
DayStatus |
Stav celého dne |
day_code |
str |
Surový kód dne z API (omezeniObj.den) |
Enumy
MealType — SOUP, MAIN, UNKNOWN. Odvozuje se z krátkého kódu druh
("PO", "O1", ...), který je stabilnější než zobrazovaný text.
Restriction — proč jídelna nedovolí jídlo objednat nebo změnit:
| hodnota | význam |
|---|---|
NONE |
Bez omezení, jídlo lze objednat |
CLOSED |
Uzávěrka objednávek na tento den už proběhla |
UNAVAILABLE |
Položku nelze objednat samostatně (typicky polévka) |
NO_SCHOOL |
Jídelna ten den nevaří |
UNKNOWN |
Jídelna vrátila kód, který knihovna nezná |
DayStatus — co jídelna říká o celém dni (pole omezeniObj.den). Na
objednatelnost nemá vliv, tu určuje vždy jen konkrétní jídlo:
| hodnota | význam |
|---|---|
NORMAL |
Běžný den |
NOT_AUTO_ORDERED |
Jídelna na tento den neobjednává automaticky |
NO_SCHOOL |
Ten den se nevaří |
CLOSED |
Uzávěrka dne už proběhla |
UNKNOWN |
Jídelna vrátila kód, který knihovna nezná |
Dny, na které se neobjednává automaticky
Jídelna některé dny označuje kódem "T". Objednat na ně jde úplně normálně, jen se
neobjednají samy — typicky pátek, kdy se ve škole neučí. Knihovna to vystavuje jako
Day.auto_ordered a jako filtr:
# Co by automat objednal, kdyby na to byl
strava.menu.get_meals(auto_ordered=True, orderable=True)
# Dny, které si musíte objednat sami
[den.date for den in strava.menu.get_days(auto_ordered=False)]
Ve výchozím nastavení se tyhle dny nijak neskrývají — jsou to obyčejné objednatelné dny.
Objednávání
from strava_cz import StravaCZ, InsufficientBalanceError, MealNotOrderableError
strava = StravaCZ("vase.jmeno", "VaseHeslo123", "3753")
strava.menu.fetch()
try:
vysledek = strava.menu.order_meals(5, 9)
print(vysledek) # "2 changed"
print(vysledek.changed) # (5, 9)
except MealNotOrderableError as e:
print(f"Tohle jídlo objednat nejde: {e}")
except InsufficientBalanceError:
print(f"Nedostatečný zůstatek: {strava.user.balance} Kč")
# Zrušení objednávek
strava.menu.cancel_meals(5, 9)
Obě metody proběhnou jako jedna transakce: nejdřív se zaškrtnou všechna jídla, pak se změny uloží najednou a nakonec se výsledek ověří na čerstvě načteném jídelníčku. Když cokoliv selže, změny se zahodí a nic se neuloží. Pokud není co měnit, neodešle se žádný požadavek.
Parametry
continue_on_error=True— neskončí u první chyby. Objedná, co jde, a zbytek vrátí vOrderResult, místo aby vyhodila výjimku.strict_duplicates=True— vyhodíDuplicateMealError, když se dvě zvolená jídla perou o stejné místo (stejný den, stejný typ). Ve výchozím stavu se objedná první z nich a u ostatních se jen vypíše varování.
vysledek = strava.menu.order_meals(5, 85, 999, continue_on_error=True)
print(vysledek.changed) # (5,) objednáno
print(vysledek.unchanged) # () už bylo objednané, nic se neposílalo
print(vysledek.skipped) # () vynecháno kvůli duplicitě
print(vysledek.failed) # ((85, MealNotOrderableError(...)), (999, MealNotFoundError(...)))
print(vysledek.ok) # False
Příkazová řádka
Balíček instaluje příkaz strava-cz. Umí všechno, co knihovna, a ve výchozím stavu
vypisuje čitelnou tabulku; pro skripty je tu --format json.
# jednou uložit přihlášení (heslo jde do klíčenky operačního systému)
strava-cz profile add skola --username vase.jmeno --canteen 3753
strava-cz menu --week # jídelníček na tento týden
strava-cz menu --orderable --type main # jen hlavní jídla, která jdou objednat
strava-cz order 5 9 # objednat
strava-cz order 5 9 --dry-run # jen říct, co by se stalo
strava-cz cancel 5 # zrušit
strava-cz ordered # co mám objednáno
strava-cz balance # 512.50 Kč
Mon 21.09.2026
· 102 Polévka Zeleninová this item cannot be ordered separately
○ 37 Oběd 1 Bramborové knedlíky s kuřecím masem a špenátem 50 Kč
○ 38 Oběd 2 Bramborové špalíky sypané cibulkou, kysané zelí 50 Kč
Fri 25.09.2026 not ordered automatically
○ 49 Oběd 1 Kuřecí asijská pánev, kari rýže, tvaroháček 50 Kč
5 days · 15 meals · 10 orderable · 0 ordered · balance 0.00 Kč
Barvy se zapínají jen na terminálu — v rouře, v souboru nebo v cronu se nevypisují,
stejně jako když je nastavená proměnná NO_COLOR. Vynutit je jde přes --color always.
Pro skripty
S --format json (nebo zkratkou --json) vypadne na standardní výstup jeden JSON
objekt a všechna hlášení jdou na chybový výstup, takže výstup zůstane zpracovatelný.
Chyba se hlásí taky JSONem, ne textem:
strava-cz --json menu --orderable | jq '.days[].meals[] | select(.type=="main") | .id'
strava-cz --json ordered | jq '.total_price'
{ "ok": false,
"command": "order",
"error": { "type": "MealNotOrderableError", "meal_id": 85, "reason": "unavailable",
"message": "Cannot order meal 85: this item cannot be ordered separately" } }
Návratové kódy — aby se šlo v shellu rozhodnout, co se pokazilo:
| kód | význam |
|---|---|
| 0 | Hotovo |
| 1 | Selhala jídelna nebo síť |
| 2 | Špatně zadaný příkaz |
| 3 | Přihlášení odmítnuto |
| 4 | Jídlo nelze objednat, zrušit, nebo neexistuje |
| 5 | Nedostatečný zůstatek |
| 6 | Problém s profilem nebo klíčenkou |
--dry-run u order a cancel neodešle vůbec nic — jen spočítá, co by se změnilo,
včetně jídel, která už jsou objednaná, a dvou jídel, která by soupeřila o stejné místo.
Úplný seznam přepínačů vypíše strava-cz --help a strava-cz menu --help.
Profily
Profil si pamatuje uživatelské jméno a číslo jídelny; heslo se do souboru nezapisuje.
Ve výchozím stavu putuje do klíčenky operačního systému (GNOME Keyring, KWallet, macOS
Keychain, Windows Credential Locker) přes balíček keyring.
strava-cz profile add skola --username vase.jmeno --canteen 3753 --verify
strava-cz profile list
strava-cz profile show skola
strava-cz profile default doma
strava-cz profile remove doma
strava-cz --profile doma menu # jednorázově jiný profil
Přepínač --secret říká, odkud se heslo bere:
| hodnota | kam se heslo uloží |
|---|---|
auto |
Výchozí. Klíčenka, když na stroji nějaká je; jinak encrypted. |
keyring |
Klíčenka systému. Do souboru se nedostane nic. |
encrypted |
Zašifrované do souboru s profily, pod heslovou frází. |
env |
Nikam. Čte se z $STRAVA_CZ_PASSWORD při každém spuštění. |
prompt |
Nikam. Zeptá se pokaždé na terminálu. |
file |
Otevřeným textem do souboru s profily. Jen když si o to řeknete. |
auto vždycky vypíše, kterou z obou cest zvolilo, a do profilu zapíše konkrétní
výsledek — profile show tedy nikdy neuhýbá.
Profily leží v $XDG_CONFIG_HOME/strava-cz/profiles.json (soubor 0600, složka 0700).
Prostředí má vždycky přednost před uloženým heslem, takže cron může použít existující
profil, aniž by se do něj sahalo: $STRAVA_CZ_PASSWORD_<JMENO_PROFILU> přebije
$STRAVA_CZ_PASSWORD a obojí přebije klíčenku.
Server bez klíčenky
Na serveru bez grafického prostředí klíčenka většinou nefunguje. strava-cz profile add
to pozná dopředu a přepne na encrypted: heslo se zašifruje algoritmem AES-256-GCM
klíčem, který z vaší heslové fráze odvodí scrypt (n=2^15, r=8, p=1). Parametry se
ukládají do záznamu, takže i po jejich pozdější změně půjdou starší profily otevřít.
"encrypted": {
"cipher": "aes-256-gcm",
"kdf": { "name": "scrypt", "n": 32768, "r": 8, "p": 1, "salt": "…" },
"nonce": "…",
"ciphertext": "…"
}
Heslová fráze se hledá stejně jako heslo: $STRAVA_CZ_PASSPHRASE_<JMENO_PROFILU>, pak
$STRAVA_CZ_PASSPHRASE, pak se na ni program zeptá na terminálu. Pro cron tedy stačí
nastavit proměnnou.
Co to řeší a co ne. Šifrování chrání soubor v klidu — zálohu, kopii domovského adresáře, složku, která se omylem odsynchronizuje jinam. Kdo umí přečíst jen
profiles.json, s ním nic nesvede. Nechrání vás před někým, kdo už čte vaše prostředí nebo paměť běžícího procesu. V cronu je heslová fráze v proměnné podobně, jako by tam bylo heslo — rozdíl je v tom, že samotný soubor s profily je pak bezpečné zálohovat a synchronizovat.
Zašifrovaný záznam je navíc ověřený (GCM), takže poškozený nebo pozměněný soubor skončí srozumitelnou chybou, ne tichým nesmyslem.
Profily jdou použít i z Pythonu:
from strava_cz import StravaCZ
with StravaCZ.from_profile("skola") as strava: # bez profilu se vezme výchozí
strava.menu.fetch()
strava.menu.print()
Výjimky
Všechny výjimky knihovny dědí ze StravaError, takže je lze odchytit najednou.
| výjimka | kdy nastane |
|---|---|
StravaError |
Základní třída všech výjimek knihovny |
StravaAPIError |
API odmítlo požadavek; nese .code, .status_code a .payload |
AuthenticationError |
Přihlášení selhalo nebo session vypršela |
NotLoggedInError |
Volání, které vyžaduje přihlášení, ale uživatel přihlášený není |
InsufficientBalanceError |
Na účtu není dost peněz |
MealNotFoundError |
ID jídla není v načteném jídelníčku |
MealNotOrderableError |
Jídelna objednávku či zrušení nedovolí; nese .reason |
DuplicateMealError |
Dvě jídla soupeří o stejné místo a je zapnuté strict_duplicates |
MenuNotFetchedError |
Přístup k jídelníčku dřív, než se zavolá fetch() |
ProfileError |
Něco je špatně s profilem nebo se souborem profilů |
ProfileNotFoundError |
Profil daného jména není uložený |
KeyringUnavailableError |
Na tomhle stroji není použitelná klíčenka |
EncryptionUnavailableError |
Chybí balíček cryptography, nelze šifrovat |
Nastavení klienta
strava = StravaCZ(
"vase.jmeno", "VaseHeslo123", "3753",
timeout=30.0, # výchozí je 15 s, z toho 10 s na navázání spojení
retries=2, # opakuje se jen spojení, které se nenavázalo
language="CS", # jazyk odpovědí z API
)
Heslo se použije jen pro přihlašovací požadavek a dál se nikde neukládá.
Parametrem client= můžete také předat vlastní httpx.Client.
To-do
- Nahrát jako knihovnu na PyPI
- Lépe zorganizovat kód
- Lepší formát data
- Možnost detailnější filtrace jídelníčku
- Kontrola stavu po objednání
- Detekce a prevence duplicitních objednávek
- Rozpoznání důvodu, proč jídlo nelze objednat
- Lépe zdokumentovat použití
- Nástroj pro příkazovou řádku
- Profily s bezpečně uloženým heslem
- Uložení hesla i na stroji bez klíčenky
- Login rate limiting
- Debug/log mód
- Asynchronní klient
Co bude dál?
Plánuji udělat aplikaci, která bude uživateli automaticky objednávat obědy podle jeho preferencí.
Prosím, nepoužívejte tuto knihovnu k nekalým účelům. Používejte ji pouze s dobrými úmysly.
Jak mi pomoct
Našel jsi chybu nebo máš návrh na zlepšení? Skvělé! Vytvoř prosím bug report nebo feature request.
Udělal jsi sám nějaké zlepšení? Ještě lepší! Každý pull request je vítaný — postup najdeš v notes/repo_rules.md.
Použití AI
Na tomto projektu byly do jisté míry využity modely LLM, primárně na dokumentaci, testy a formátování kódu. Každá taková úprava prošla mojí kontrolou.
Licence
Metadata
Release files for strava-cz 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| strava_cz-0.4.0.tar.gz | 87.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| strava_cz-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 152.6 kB
Release files / strava_cz-0.4.0.tar.gz
| Download URL | strava_cz-0.4.0.tar.gz |
|---|---|
| Size | 87.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0613e9e66c00c0ef50fc7db4abd0a67996a7f37a40f0c2555a957587c5667526
|
|
BLAKE2b-256 checksum How to use checksums |
3cfd7beee75bbf8b4c4d7ac32a9611c8073617531a5b35259390381a18773f72
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 2, 2026.
Transparency logRelease files / strava_cz-0.4.0-py3-none-any.whl
| Download URL | strava_cz-0.4.0-py3-none-any.whl |
|---|---|
| Size | 65.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6b5b685513c3f8695897c401d6dab860950d023776314b07e50d8ba10d5b861f
|
|
BLAKE2b-256 checksum How to use checksums |
140ebe07e3191179caca743ee4b73eac9522ef446c19fe2b80af35b223416068
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 2, 2026.
Transparency log