So erstellen Sie schnelle, zuverlässige APIs aus langsamen, unzuverlässigen Websites

Für ein kürzlich durchgeführtes Projekt mussten wir einen Workflow aus mehreren verschiedenen Bioinformatik-Websites automatisieren, die niemals automatisiert werden sollten. Dies warf einige interessante Probleme auf, die wir am Ende ganz gut gelöst haben. Ich werde die Probleme und Lösungen hier detailliert beschreiben, damit Sie es vermeiden können, unsere Bemühungen zu wiederholen, wenn Sie sich jemals in einer ähnlichen Situation befinden.

Hintergrund

Das fertige Produkt sollte ein Web-Tool sein, mit dem Wissenschaftler des Chan Zuckerberg Biohub und anderer Länder CRISPR-Bearbeitungen im Batch entwerfen und analysieren konnten (jetzt live unter crispycrunch.czbiohub.org). Mit den rasanten Fortschritten auf dem Gebiet der Genomtechnik entwickeln Wissenschaftler immer größere Experimente, bei denen die Automatisierung zum Einsatz kommen muss. Im Biohub fügt das Team von Manuel Leonetti fluoreszierende Proteine ​​in alle 22.000 proteinkodierenden Gene des menschlichen Genoms ein. Ebenso bearbeitete Ryan Leenay 1.521 verschiedene Genomorte, um ein Modell zu trainieren, das die Ergebnisse der Bearbeitung in T-Zellen des Immunsystems vorhersagt. Um Experimente dieser Größenordnung durchführen zu können, mussten die Wissenschaftler ihre manuellen Abläufe automatisieren. Teil der Mission von CZI ist es, moderne Ingenieur- und Produktmethoden anzuwenden, um die biologische Forschung zu beschleunigen.

Softwarearchitektur

Die Wissenschaftler, mit denen wir zusammengearbeitet haben, verwendeten bereits zwei wichtige Web-Tools in ihrer CRISPR-Arbeit: Crispor und Crispresso. Ein Wissenschaftler entwarf einen CRISPR-Schnitt in Crispor, führte sein Experiment im Nasslabor durch und analysierte dann die Ergebnisse mit Crispresso. Unsere Software namens CrispyCrunch (hier ein Thema?) Vereint die beiden Funktionen Design und Analyse und ermöglicht die Stapelverarbeitung - bis zu 96 Bearbeitungen in einem Experiment, die Standardgröße einer Zellkulturplatte.

Wie CrispyCrunch von externen Tools abhängt

Wir haben uns entschieden, CrispyCrunch auf den vorhandenen Werkzeugen von Crispor und Crispresso aufzubauen, da unsere Wissenschaftler ihnen bereits vertraut haben. Wir wollten einen funktionsfähigen Prototypen so schnell wie möglich, den wir später durch unsere eigenen Komponenten ersetzen können. Das Risiko lag in der Integration. Diese Tools waren nicht als Teil eines größeren Systems konzipiert.

Um Erlaubnis fragen

Als Vorwort zu allen folgenden technischen Hacks sollten Sie erwähnen, dass wir die Eigentümer von Crispor und Crispresso frühzeitig gefragt haben, ob sie damit einverstanden sind, dass wir ihre Websites als APIs behandeln. Wir hatten zwar nicht damit gerechnet, dass sie für uns arbeiten würden, hofften jedoch, dass sie CrispyCrunch nicht beim ersten Anzeichen von Problemen blockieren würden. Gerne stimmten Luca und Max zu.

Problem # 1: Wie man eine API aus einer Website macht

Dies ist das klassische Problem des Web Scraping, auf das fast jeder Entwickler früher oder später stößt. Das Problem besteht nicht darin, die Daten herauszuholen - dies ist mit Bibliotheken wie BeautifulSoup, die Daten von Webseiten durch CSS-Selektoren extrahieren, einfach -, sondern darin, die Ergebnisse zuverlässig und fehlerbehaftet zu machen.
 
 Zu diesem Zweck verwendeten wir die folgenden Techniken:

  • Verwenden Sie HTML-Selektoren, deren Änderung am unwahrscheinlichsten ist. Bevorzugen Sie beispielsweise HTML-IDs gegenüber Elementtypen.
  • Wählen Sie die gewünschten Daten aus dem DOM direkt nach Klassennamen oder IDs aus. Vermeiden Sie es, untergeordnete oder übergeordnete Elemente auszuwählen, die möglicherweise die Position in der Dokumenthierarchie ändern.
  • Verlassen Sie sich beim POST nicht auf Standardwerte, die sich ändern können. Geben Sie explizite Werte für alle Felder an.
  • Lernen Sie alle möglichen Arten von Fehlern kennen, indem Sie die Parameter GET und POST systematisch variieren.
  • Heraufstufen von Zeichenfolgenfehlern zu Ausnahmen. Eine HTTP-Anfrage kann 200 OK zurückgeben, während der Web-Seitentext einen offensichtlichen "FEHLER !!!" enthält.
  • Verwenden Sie Behauptungen, um Annahmen zu kodifizieren. Nehmen Sie beispielsweise an, dass einige HTML-Listen eine Anzahl von Elementen ungleich Null haben.
  • Erwarte das Unerwartete. Melden Sie sich reichlich an und lassen Sie sich über Ausnahmen per E-Mail oder von einem Dienst wie Sentry benachrichtigen.
  • Versuchen Sie erneut, zeitweise auftretende Fehler zu umgehen. Verwenden Sie das Backoff wie unten beschrieben.

Problem Nr. 2: Erhöhen des Durchsatzes eines API-Clients

Nachdem wir die Basisfunktionalität hatten, die wir brauchten, bestand das nächste Problem darin, sie schnell zu machen. Die naheliegende Lösung bestand darin, unabhängige Anfragen parallel zu senden.
 
 Mit Python und der allgegenwärtigen Anforderungsbibliothek können Sie etwas so Einfaches tun:

Importanforderungen
aus concurrent.futures importieren Sie ThreadPoolExecutor
Pfade = [
    '/ view_report / mNGplate1',
    '/ view_report / mNGplate2',
    '/ view_report / mNGplate3',
    # ...Mehr
]
def _request (path):
    return questions.get ('http://crispresso.rocks' + Pfad)
mit ThreadPoolExecutor () als Pool:
    results = pool.map (_request, path)

Hinweis: ThreadPoolExecutor ist in der Regel effizienter als ProcessPoolExecutor für Webanforderungen, da ein Thread weniger Speicher als ein Prozess benötigt und die Python-GIL nicht relevant ist, wenn E / A blockiert werden.

Problem Nr. 3: Wie ein API-Client einen teilweise erfolgreichen Stapel fortsetzen kann

Wenn ein Anforderungsstapel länger als ein paar Sekunden dauert, ist es wünschenswert, in der Lage zu sein, neu zu starten, ohne den bisherigen Fortschritt zu verlieren. Dies ist in mehreren gängigen Situationen wünschenswert:

  • Zeitweiliger Ausfall
  • Debuggen
  • Der Benutzer muss zurückgehen und etwas ändern

Auch hier ist die Lösung einfach: Zwischenspeichern von HTTP-Antworten, sodass eine Anforderung für eine zuvor heruntergeladene Ressource eine zwischengespeicherte Kopie zurückgibt. Beim Neustart eines Stapels werden nur unvollständige Anforderungen erneut ausgeführt.
 
 Für das Caching haben wir das praktische request_cache-Modul verwendet. Einige Tipps zur Verwendung:

  • Erstellen Sie im Gegensatz zum Lernprogramm ein explizites Cache-Sitzungsobjekt, um Nebenwirkungen zu vermeiden.
  • Sie sollten den Cache für eine URL explizit löschen, wenn Sie feststellen, dass der Inhalt irgendwie ungültig ist, damit Sie es mit einer neuen Anfrage erneut versuchen können. Mit anderen Worten, Fehler, die keine HTTP-Fehler sind, müssen speziell behandelt werden.
  • orders_cache speichert keine POST-Anforderungen, die Dateien enthalten, da die Dateibegrenzung eine zufällige Zeichenfolge ist. Sie können das beheben, indem Sie urllib3 mit monkey-patching aktualisieren. (Dies hat eine Weile gedauert, um herauszufinden!)
urllib3.filepost.choose_boundary = lambda: ‘crispycrunch_super_special_form_boundary’
  • Der Remote-Server speichert möglicherweise Antworten zwischen - manchmal auf eine Weise, die Sie nicht möchten. Beispiel: Eine der APIs, die wir für zwischengespeicherte Fehlerantworten verwendet haben. Der Fix bestand darin, eine zufällige Zeichenfolge an die URL anzuhängen, um sie aus der Sicht des Servers immer neu zu machen, und request_cache anzuweisen, den zufälligen Parameter zu ignorieren das gleiche aus Sicht des Kunden.

Problem Nr. 4: Wie kann ein API-Client isoliert getestet werden?

Wenn Sie über einen komplexen Code zum Parsen von Antworten verfügen, sollten Sie Komponententests durchführen, die Sie isoliert vom Internet ausführen können. Das Problem ist, dass das Erstellen von Testdaten zeitaufwändig und umständlich sein kann. HTML-Seiten können Hunderte von KB groß sein.
 
 Caching kommt wieder zu Hilfe. Sie können request_cache verwenden, um eine vollständige Antwort in SQLite zu speichern und die SQLite-Datei anschließend der Versionskontrolle zuzuführen. Sofortige Testisolierung! Variieren Sie den Namen der Datei im Testmodus und fügen Sie ihn als Ausnahme zu Ihrer gitignore-Datei (oder einer entsprechenden Datei) hinzu. Hinweis: Sie können mit dem Attribut from_cache überprüfen, ob eine Anforderung aus dem Cache stammt.

Problem # 5: Wie Sie Ihren Client dazu bringen, die API nicht zum Absturz zu bringen

Sogar offizielle Web-APIs können durch überraschend wenig Verkehr in die Knie gezwungen werden. Die einzige Lösung für dieses Problem besteht darin, die API einem Stresstest zu unterziehen, damit Sie die Datenverkehrsbeschränkungen erkennen, bevor Ihre Benutzer dies tun, und die Rate entsprechend zu begrenzen - siehe unten, Nr. 7. Hinweis: Möglicherweise möchten Sie den API-Eigentümer benachrichtigen, bevor Sie dies tun!
 
 Für Stresstests können Sie das Befehlszeilentool Apache Bench (ab) verwenden. Hinweis: Die Server selbst können auf verschiedene Arten zwischengespeichert werden. Achten Sie daher darauf, dass der Auslastungstest realistisch ist.

Problem Nr. 6: Vermeiden von Racebedingungen bei API-Aufrufen

Ein unangenehmes Problem, das wir beim Verketten von API-Aufrufen festgestellt haben, war das zeitweise Auftreten von 404s. Dies ist symptomatisch für eine Race-Bedingung, in der eine Ressource nicht bereit ist, obwohl vorausgehende HTTP-Antwortcodes dies implizieren. Solche Wettkampfbedingungen werden möglicherweise erst auf einer Website bekannt gegeben, wenn sie programmgesteuert verwendet werden, da ein menschlicher Benutzer nicht so schnell wie ein Computer-Client agieren kann.
 
 Die Lösung für die Rennbedingungen in einem System, das Sie nicht kontrollieren, ist entweder:

  • Führen Sie eine künstliche Verzögerung ein, wie z
Importzeit; Schlafenszeit (1)
  • Blockieren Sie die Ressource, die verfügbar wird
Importanforderungen
def _request (path):
 request.get zurückgeben ('http://crispresso.rocks' + Pfad)
while _request (path) .status == 404:
 print (f’Warten auf {path} ’)
 Schlafenszeit (1)

Hinweis: Wenn Sie die Antworten wie oben beschrieben zwischenspeichern, zahlen Sie die Hin- und Rückkosten einer zusätzlichen Anforderung nicht, wenn Sie nicht warten müssen.

Problem Nr. 7: Wie man nur unter den API-Ratengrenzen bleibt

Einige APIs sind möglicherweise nett genug, um zu melden, wenn Sie sie zu stark beanspruchen. Wenn Sie innerhalb des Limits optimieren möchten, können Sie einen clientseitigen Ratenbegrenzer wie ratelimit verwenden. In Kombination mit einem exponentiellen Backoff können Sie einen respektvollen und performanten Kunden haben.

Importanforderungen
von ratelimit import limits, RateLimitException
von backoff import on_exception, expo
@on_exception (expo, RateLimitException, max_tries = 8)
@limits (Anrufe = 15, Periode = 900) # 15min
def call_api (url):
    request.get (url) zurückgeben

Passen Sie Aufrufe und Zeitraum an das beobachtete oder angegebene Ratenlimit der API an.

Fazit

Dies sind die Probleme, die wir lösen mussten, damit CrispyCrunch reibungslos mit den Crispor- und Crispresso-Webtools funktioniert, die niemals als Webservices gedacht waren. Wir hoffen, Sie finden die Details hilfreich. Wenn Sie mehr über CZI Engineering erfahren möchten, besuchen Sie unsere Technologie-Seite.