Wenn du mit Python APIs aufrufst, nutzt du wahrscheinlich das requests Modul. Manchmal sind API Aufrufe jedoch mit Kosten (Rechenleistung, Bandbreite, RAM etc.) verbunden. In solchen Fällen kannst du eine Technik nutzen, die als Caching bezeichnet wird.

Beim Caching werden Daten, die teuer beim Einlesen sind, in einen günstigen Zwischenspeicher geschrieben und erst dann erneut abgefragt, wenn bestimmte Bedingungen erfüllt sind. Eine Bedingung kann zum Beispiel sein, dass die Daten im Cache ein bestimmtes Alter erreichen.

Ein Modul das sich nahtlos an das requests Modul andockt und automatisch das Caching all deiner API Aufrufe übernimmt, nennt sich requests-cache. Du findest es auch als Projekt auf Pypi. In diesem Tutorial möchte ich es dir vorstellen und zeigen wie du es einsetzen kannst um in Zukunft Aufrufe einsparen zu können

Cache Prinzip

Wenn du einen requests-cache nutzt, schaut dein Programm zuerst in diesen Cache und überprüft, ob die angefragten Daten bereits vorhanden und noch nicht zu alt sind. Werden entsprechende Daten gefunden, nutzt das Programm einfach diese. Siehe folgendes Schaubild.

Erst, wenn keine oder zu alte Daten im Cache vorliegen, wird ein Versuch unternommen die Daten von der API zu besorgen.

Einsatzgebiet

Wie Eingangs erwähnt ist das Haupteinsatzgebiet von requests-cache die Anbindung an teure APIs. Nehmen wir einmal an du arbeitest mit einer eine API, die Wetterdaten bereitstellt zusammen, bei der du für jeden Aufruf bezahlst.

Durch diese Kosten hast du sicherlich ein Interesse daran möglichst selten Aufrufe zu machen. Da sich das Wetter in wenigen Sekunden kaum ändert, wäre es ausreichend nur noch einen Aufruf pro Minute zu tätigen.

Und selbst wenn du gar nichts für deine Aufrufe bezahlen würdest, wärst du dennoch gut beraten, wenn du die Antworten cachest. Dadurch verhinderst du nämlich unnötige Last auf beiden Seiten. Denn auch dein Programm wird schneller laufen, wenn es nicht auf externe APIs warten muss.

Installation

Requests kann mit pip installiert werden. Das geht mit folgendem Befehl sowohl unter Linux als auch unter Mac und Windows.

pip install requests-cache

Anschließend kann requests-cache in dein Pythonprojekt importiert werden. Beachte den Unterstrich anstelle des Bindestrichs!

import requests_cache

Python und Pip müssen natürlich installiert sein.

Nutzung

Prinzipiell bietet requests-cache 2 Varianten zur Implementation an. Den globalen, also programmweiten Cache sowie die Cached Session.

Globaler Cache (install_cache)

Die einfache Variante ist, es mithilfe von requests-cache die Library von requests patchen zu lassen. Dadurch wird quasi ein programmweit gültiger Cache für alle nachfolgenden requests Aufrufe erzeugt.

import requests
import requests_cache

requests_cache.install_cache('IRGENDEINNAME')
requests.get('https://quisl.de/')

Mit zusätzlichen Parametern kann der Cache konfiguriert werden. Etwa welches Backend er verwendet, welche Aufrufe cached werden und wann die Daten frühstens upgedatet werden sollen. Dazu später mehr. Mit den Defaultwerten wird eine SQLite Datenbank mit dem angegebenen Namen im aktuellen Arbeitsverzeichnis erzeugt.

Von der Nutzung dieser globalen Variante würde ich generell abraten. Schließlich werden dadurch nämlich sämtliche requests Aufrufe deines Programms beeinflusst. Vor allem bei großen Projekten wie etwa Webseiten mit Django kann dies zu ungewollten Nebeneffekten führen.

Cached Session (CachedSession)

Eine bessere Lösung bietet die cached Session. Dabei wird ein Objekt erzeugt, das sich genauso wie eine requests.session verhält. Darüber hinaus unterstützt dieses Objekt caching und kann dieselben Parameter wie auch der globale Cache entgegennehmen.

import requests_cache

session = requests_cache.CachedSession('IRGENDEINNAME')
session.get('https://quisl.de/')

Ein weiterer Vorteil an dieser Variante ist, dass du requests gar nicht mehr importieren musst. Dadurch sparst du zumindest in diesem Beispiel eine Zeile Code.

Backend (Parameter: backend)

Aktuell unterstützt requests-cache folgende Backend Datenbanken:

  • DynamoDB
  • Filesystem
  • GridFS
  • MongoDB
  • Redis
  • SQLite

Wobei letztere mit SQLite die default Variante ist, die verwendet wird, wenn keine Datenbank explizit angegeben wurde. Dabei wird für jeden neuen Namen der CachedSession eine eigene Datei bzw. Datenbank im aktuellen Arbeitsverzeichnis erzeugt. Dadurch wird ermöglicht, dass du dein Programm neu starten kannst und immer noch auf den Cache von vor dem Neustart zugreifen kannst. Sofern die Dateien noch immer dort sind.

Solang du keinen geteilten Cache über mehrere Server hinweg benötigst, würde ich bei SQLite bleiben. Brauchst du den geteilten Cache doch, musst du dich mit einer der anderen Datenbanken vertraut machen und entsprechend einen Server für das Caching aufsetzen. Ich denke Redis wäre als in-Memory Datenbank eine geeignete Wahl.

Zum Konfigurieren des Backends kannst du einfach den backend Parameter benutzen.

import requests_cache

session = requests_cache.CachedSession('IRGENDEINNAME', backend="sqlite")
session.get('https://quisl.de/')

Zeit (Parameter: expire_after)

Natürlich wollen wir auch noch definieren wie lang die Daten im Cache vorgehalten werden sollen. Dazu dient der expire_after Parameter.

Dieser kann folgende Werte verarbeiten:

  • -1 (niemals ablaufen)
  • 0 (sofort ablaufen (um den Cache zu überbrücken)
  • eine positive ganze Zahl (Alter in Sekunden)
  • ein timedelta (aus dem datetime Modul)
  • ein datetime Zeitpunkt

Folgender Code erzeugt einen Cache der die Daten bis zu 6 Minuten speichert.

import requests_cache

session = requests_cache.CachedSession('IRGENDEINNAME', expire_after=360)
session.get('https://quisl.de/')
session = CachedSession()

Sonstige Parameter

Hier noch ein paar der wichtigsten Parameter, die du beim Erstellen einer CachedSession mitgeben kannst im Überblick.

cache_name (str) – Cache prefix oder namespace, je nach Backend
backend (str) - Backend Typ. Einer von: ['sqlite', 'filesystem', 'mongodb', 'gridfs', 'redis', 'dynamodb', 'memory']. Manche Backend Typen brauchen noch weitere Parameter. Siehe offizielle Doku.
expire_after (None, int, float, str, datetime, timedelta) – Zeit nach der gecachte Daten auslaufen.
urls_expire_after (dict) – Wann Daten bestimmter URL Muster auslaufen
cache_control (bool) – Benutze den Cache-Control Header um das Ablaufdatum zusetzen. Ist zum Beispiel nützlich bei Auth Token bei denen du im vorraus nicht genau weiß wann sie ablaufen.
allowable_codes (int) – nur bestimmte HTTP Statuscodes cachen
allowable_methods (Iterable[str]) – Nur bestimmte HTTP Methoden cachen (zum Beispiel: ("GET", "POST", "PUT", "DELETE"))
match_headers – Nur Pakete cachen die bestimmte Header haben. Diese müssen als Header Liste (oder zumindest als Iterable) mitgegeben werden. (True speichert alle Header)

Dazu noch einige spezielle Parameter je nach Backend.

Weitere Beispiele findest du in der offiziellen Doku.


Konnte ich helfen? Ich freue mich über einen Drink! 💙