Saurier-Tech (Linux-Magazin, Dezember 2026)

Statt seine Auktionen auf Ebay mit dem Webbrowser zu überwachen, baut Mike Schilli mit Hilfe der offiziellen Ebay-Developer-API ein Dashboard fürs Terminal.

Ebays Webauftritt ist in den 90ern steckengeblieben, und knarzt und knirscht überall. Wer ein Listing ändert, reibt sich die Augen darüber, dass es noch minutenlang im großen Ebay-Cache über die ganze Seite verteilt falsch hochkommt. Aber das Auktionshaus, das heutzutage über 90% seiner Angebote zum Festpreis verkauft und weniger durch Gebote Ersteigertes, bietet eine breite Auswahl an Waren, die es nicht mehr zu kaufen gibt. Ein gebrauchtes Ersatzteil für einen Staubsauger für ein paar Euro, das beim Hersteller ein Vermögen kostet? Eine Toner-Kartusche für einen 30 Jahre alten Laserprinter? Oder ein unmöglich zu bekommendes Ersatzteil für einen 30 Jahre alten Oldtimer? Ein Schrotthändler im Bundestaat Indiana hat's und bietet es auf Ebay feil. Ebay hält damit einen Markt am laufen, der einen wertvollen Beitrag zur Nachhaltigkeit leistet.

Auch ich verscherble laufend alten Krempel für wenig Geld auf Ebay, wenn ich weiß, dass vielleicht irgendjemand irgendwo das entsprechende Ersatzteil sucht. Große Gewinne erwarte ich dabei nicht, denn Ebay zweigt für jede Transaktion gute 20% Gebühren ab. Auf der Käuferseite bin ich andererseits dankbar, wenn ich mal wieder ein dringend gesuchtes Teil zum Schlagerpreis erwerben kann, weil es jemand im hintersten Winkel Amerikas feilbietet.

Abbildung 1: Anzeige laufender Ebay-Auktionen im Terminal

Saurier-Tech

Meinen Warenstand und Bestellungen im Webbrowser zu verfolgen ist wegen des Saurier-Interfaces auf ebay.com etwas mühsam, deswegen klopfte ich mir eine Terminal-Applikation zusammen. Damit Ebay den Zugriff auf meine User-Daten erlaubt, musste ich mich als Entwickler einer Applikation erstmal auf developer.ebay.com registrieren und erhielt erst zwei Tage später per Email die (wohl automatisch generierte) Genehmigung zum Betrieb einer selbstgetrickten App.

Den von der App abgefeuerten API-Abfragen muss jeweils ein gültiger User-Token beiliegen. Der weist den Absender auf dem Internet als legitimen User einer Applikation aus. Unter seiner Ägide feuert die App Abfragen zum eigenen Ebay-Account ab. Damit Ebay das erlaubt, muss der App-User einmal im sogenannten Oauth-Token-Dance zustimmen, dass die App unter seinem Namen Ebay-Daten seines Accounts einholen darf. Auch schreibender Zugriff ist möglich, wenngleich die aktuell vorgestellte App nur liest.

Statt Sandbox gleich Produktion

Dabei erlaubt Ebay Tests in einer "Sandbox", einem abgeschirmten Bereich, der weder Live-Auktionen noch tatsächliche Usernamen kennt. Da die aktuell vorgestellte App nur lesend auf die Privatdaten des eingeloggten Users zugreift, habe ich sie gleich forsch in der Produktionsumgebung getestet. Auf developer.ebay.com/my/keys zeigt Ebay die aktuell genehmigten Schlüssel (Abbildung 2).

Abbildung 2: Beantragte und genehmigte Application-Keys für Sandbox und Production.

Ein Klick auf "User Tokens" in Abbildung 2 unter der App ID der gewählten Umgebung bringt eine Seite hoch, die im oberen Bereich Access-Tokens für die App austellt, die aber nur zwei Stunden gültig bleiben. Eine langlebige App benötigt zusätzlich einen Refresh-Token, den sie beliebig oft gegen aktuell gültige Access-Tokens eintauschen darf.

Die (etwas fragwürdige) Idee dahinter ist, dass bei aktivem App-Betrieb der Access-Token jedem API- Request beiliegt, während der Refresh-Token seltener das gerade genutzte Gerät verlässt. Unsere heutige App wird nur sporadisch aufgerufen, und holt deshalb bei jedem Aufruf einen neuen Access-Token ab, indem sie den Refresh-Token ausspielt.

Abbildung 3: Von der Autorisierung der App, zum Refresh-Token, zum Access-Token

Kurzer Prozess dank Github

Wie kommt die App nun an den Refresh-Token, der ihr unendlich viele neue Access-Tokens verheißt? Dazu muss eine Pseudo-App durch den Token-Dance marschieren (Abbildung 3), und das geht dank dem Projekt oauth-cmdline-go auf Github ruck-zuck. Die Instruktionen in Abbildung 4 installieren das Tool, und rufen es mit dem Kommando authorize auf worauf es einen lokalen SSL-Server hochfährt, auf den der User den Browser einnordet.

Abbildung 4: Schritte zum Einholen des Refresh-Tokens für Ebays Developer-API

Der Browser merkt natürlich sofort, dass der Ad-Hoc-Server sich nur mit einem selbstsignierten Zertifikat ausweist und bringt eine Warnung hoch (Abbildung 5). Daran ist aber nichts verkehrt, und der User darf getrost auf den Button "Advanced" klicken, worauf eine weitere Warnung erscheint (Abbildung 6), die mit dem unten erscheinenden Button "Proceed to https://localhost:8082 (Risky)" wegzuklicken ist. Nun kommt der Genehmigungs-Flow auf Ebay hoch, der den User dazu auffordert, sich einzuloggen falls dies noch nicht geschehen ist (Abbildung 7).

Abbildung 5: Wegen des selbstsignierten Server-Zertifikats bringt der Browser eine Warnung ...

Abbildung 6: ... die der User aber getrost ignorieren darf.

Abbildung 7: Der User muss sich auf Ebay einloggen und die App autorisieren.

Anschließend bringt Ebay Details zur App hoch, die der Entwickler vorher bei der Registierung abgelegt hat. Klickt der User dann im Flow auf "Authorize", schickt Ebay den Browser wieder zur redirect_uri zurück. Das ist im vorliegenden Fall der lokale Server auf https://localhost:8082, der sich dank oauth-cmdline-go die zurückkommenden Tokens schnappt und in ebay.yaml auf der Platte ablegt.

Ebay macht's kompliziert

Dieser redirect_uri ist bei vergleichbaren API-Services einfach ein URL, im einfachsten Fall ein lokaler Server ohne SSL, zum Beispiel http://localhost:8082/callback. Dieser URL wird bei der Anmeldung der App auf dem Registrierungsserver abgelegt. Ebay macht das aber doppelt kompliziert, denn erstens nutzt es statt einem URL einen sogenannten "RuName", einen generierten Namensstring, der seinerseits unter "Your auth accepted URL" einen URL setzen darf (Abbildung 8). Vor der Autorisierung der App in Abbildung 4 ist in der Konfigurationsdatei ebay.yaml deshalb der RuName im Feld redirect_uri zu setzen und nicht der URL. Zweitens besteht Ebay auf einem Server mit SSL, denn in das Formularfeld unten in Abbildung 8 darf nur mit "https://" beginnen. Das verursacht bei der Autorisierung den oben beschriebenen Affentanz um die Browserwarnung, aber wir müssen uns vergegenwärtigen, dass eine Kommandozeilen-App die Ausnahme und nicht die Regel ist.

Abbildung 8: Nach der Authorisierung geht's zurück an den lokalen Server

Abbildung 9: Der lokale Hilfsserver hat den Access-Token erhalten und gespeichert.

Nachdem der Users die neue App für seinen Ebay-Account autorisiert hat, verzweigt der Browser wieder zurück zum temporären lokalen Server auf Port 8082, der wie in Abbildung 9 gezeigt "Tokens saved" anzeigt und die Tokens zu den vorher in ebay.yaml abgelegten Werten hinzufügt. Die App kann nun bei jedem neuen Aufruf von der Kommandozeile den refresh_token aus ebay.yaml extrahieren, ihn auf dem Ebay-Oauth-Server gegen einen Access-Token eintauschen und damit in einem weiteren Request Userdaten einholen.

Listing 1: ebay_api.py

    01 from lxml import etree
    02 import requests
    03 from ebay_data import EBAY_NS, element
    04 BASE = "https://api.ebay.com"
    05 class Ebay:
    06   def __init__(self, config):
    07     self.config = config
    08     self.token = ""
    09     self.http = requests.Session()
    10   def refresh(self):
    11     c = self.config
    12     fields = ("client_id", "client_secret", "refresh_token", "token_uri")
    13     credentials = [c.get(key) for key in fields]
    14     if not all(credentials):
    15       raise RuntimeError("Update config with " + ", ".join(fields))
    16     response = self.http.post(
    17       c["token_uri"], timeout=30,
    18       auth=tuple(credentials[:2]),
    19       data={"grant_type": "refresh_token",
    20          "refresh_token": credentials[2]})
    21     response.raise_for_status()
    22     self.token = response.json()["access_token"]
    23   def call(self, name, data=None):
    24     root = etree.Element(name + "Request", xmlns=EBAY_NS)
    25     for key, val in (data or {}).items():
    26       element(root, key, val)
    27     if not self.token:
    28       self.refresh()
    29     response = self.http.post(
    30       BASE + "/ws/api.dll", timeout=30,
    31       data=etree.tostring(root, encoding="utf-8", xml_declaration=True),
    32       headers={"Content-Type": "text/xml",
    33           "X-EBAY-API-CALL-NAME": name,
    34           "X-EBAY-API-SITEID": "0",
    35           "X-EBAY-API-COMPATIBILITY-LEVEL": "1379",
    36           "X-EBAY-API-IAF-TOKEN": self.token})
    37     response.raise_for_status()
    38     return etree.fromstring(response.content)

Listing 1 zeigt die Implementierung des API-Clients in Python. Die Methode refresh() ab Zeile 10 schaut in die Daten der vorher geöffneten Konfigurationsdatei ebay.yaml und greift sich den Refresh-Token und den URL des Token-Servers auf Ebay. Daraus baut sie einen API-Request zusammen, setzt ihn ab, und holt aus den zurückkommenden JSON-Daten den Access-Token, den sie für zukünftige Requests in der Instanzvariablen token ablegt.

Diese API-Requests führt die Methode call() ab Zeile 23 aus. Später wird Listing 2 auf diesem Weg die API-Methoden GetUser (Name des Ebay-Users), GetOrders (Bestellungen) und GetMyeBaySelling (Verkäufe des Users) aufrufen, jeweils Antworten in XML erhalten und relevante Daten für die Anzeige herausfieseln. Den Namen der API-Methode erwartet Ebays API-Server im Header X-EBAY-API-CALL-NAME und der ebenfalls gesetzte Compatibility-Level bestimmt die Version der API, die gerade noch mit dem Client-Code funktioniert.

Abbildung 10: Die Abfrage bringt die XML-Antwort, die ein Parser aufdröseln muss.

77 für Deutschland

Der Wert des Headers X-EBAY-API-SITEID bestimmt, an welche Ebay-Abteilung in welchem Land die Abfrage geht. Der Namensraum der Ebay-Usernamen ist weltweit einheitlich, aber Käufe und Verkäufe eines Users werden für jedes Land separat geführt. Der Wert 0 für die SITEID gilt für das Ebay-Mutterhaus in den USA. Für Transaktionen auf Ebay.de in Deutschland ist der Wert 77 für SITEID anzugeben.

Python oder Go?

Warum wohl die Wahl der Programmiersprache in dieser Ausgabe auf Python statt auf Go fiel? Das Projekt hätte sich natürlich auch in Go realisieren lassen, allerdings grob mit der doppelten Anzahl der Zeilen in den Listings. Python macht mit Web-Requests, XML- und JSON-Parserei kurzen Prozess, und fängt Fehler zentral durch Exceptions ab, statt den Aufruf jeder Funktion auf Fehler zu prüfen. Was dabei für die Wartbarkeit eines Projekts besser ist, ist umstritten und letztendlich Geschmackssache. Was enorm Platz beim Programmieren spart, sind Pythons Inline-for-Schleifen wie zum Beispiel in Zeile 7 von Listing 2, wo for einen Array erzeugt, der dann nach einem join gleich der Funktion xpath() übergeben wird. Das zwar sehr kompakt, aber Go-Enthusiasten würden anmerken, dass sich das nicht so flüssig lesen lässt wie eine ordentlich formatierte for-Schleife. Das Einrücken von Codeblöcken statt sie in geschweifte Klammern einzuschließen spart ebenfalls Platz, beim Editieren muss man allerdings aufpassen wie ein Haftelmacher, damit die Syntax erhalten bleibt.

Auch das Jonglieren mit Datentypen ist in Python einfacher. Ein per XML hereinkommender String ist flugs in einen numerischen Geldbetrag umgewandelt. Und natürlich bietet Python auch jede Menge schlüsselfertiger Pakete, ob zum Einholen von API-Antworten oder das Herumsuchen im XML nach Datenfutter. Wie die XML-Antwort in Abbildung 10 zeigt, liegen die Ergebnisfelder im Namensraum urn:ebay:apis:eBLBaseComponents, und Listing 2 definiert ab Zeile 6 die Hilfsfunktion nodes, die ein Feld unter seinem Namen mittels eines XPath-Queries aus dem Datensalat herausfieselt.

Listing 2: ebay_data.py

    01 from collections import defaultdict
    02 from datetime import datetime, timedelta, timezone
    03 from decimal import Decimal
    04 from lxml import etree
    05 EBAY_NS = "urn:ebay:apis:eBLBaseComponents"
    06 def nodes(node, path):
    07   return node.xpath("/".join(f"ebay:{name}" for name in path.split("/")),
    08            namespaces={"ebay": EBAY_NS})
    09 def value(node, path, default=""):
    10   matches = nodes(node, path)
    11   return (matches[0].text or "") if matches else default
    12 def element(parent, name, data):
    13   node = etree.SubElement(parent, name)
    14   if isinstance(data, dict):
    15     for key, val in data.items():
    16       element(node, key, val)
    17   else:
    18     node.text = str(data)
    19   return node
    20 def pages(api, name, data, path, key, container=""):
    21   found = {}
    22   for page in range(1, 1001):
    23     target = data[container] if container else data
    24     target["Pagination"] = {"EntriesPerPage": 100, "PageNumber": page}
    25     result = api.call(name, data)
    26     for node in nodes(result, path):
    27       found[value(node, key)] = node
    28     total = value(result, "ActiveList/PaginationResult/TotalNumberOfPages",
    29            "0")
    30     more = (page < int(total) if container else
    31         value(result, "HasMoreOrders") == "true")
    32     if not more:
    33       return list(found.values())
    34   raise RuntimeError("Too many pages")
    35 def fetch(api, days):
    36   now = datetime.now(timezone.utc)
    37   root = api.call("GetUser")
    38   users = nodes(root, "User")
    39   if not users:
    40     raise RuntimeError("GetUser failed")
    41   listings = pages(api, "GetMyeBaySelling", {
    42     "ActiveList": {"Include": "true"}},
    43     "ActiveList/ItemArray/Item", "ItemID", "ActiveList")
    44   orders = []
    45   for role in ("Seller", "Buyer"):
    46     rows = pages(api, "GetOrders", {
    47       "OrderRole": role, "OrderStatus": "All",
    48       "NumberOfDays": days
    49     }, "OrderArray/Order", "OrderID")
    50     orders.append(sorted(rows, key=lambda o: value(o, "CreatedTime"),
    51               reverse=True))
    52   listings.sort(key=lambda i: value(i, "ListingDetails/EndTime"))
    53   return users[0], listings, *orders
    54 def paid(order):
    55   canceled = value(order, "OrderStatus") in ("Cancelled", "Canceled")
    56   return bool(value(order, "PaidTime")) and not canceled
    57 def unshipped(order):
    58   lines = nodes(order, "TransactionArray/Transaction")
    59   return (paid(order) and not value(order, "ShippedTime") and
    60       (not lines or any(not value(t, "ShippedTime") for t in lines)))
    61 def totals(orders):
    62   sums = defaultdict(Decimal)
    63   for order in orders:
    64     for amount in nodes(order, "Total")[:1]:
    65       if paid(order) and amount.text:
    66         sums[amount.get("currencyID", "?")] += Decimal(amount.text)
    67   return " / ".join(f"{c} {v:.2f}" for c, v in sorted(sums.items())) or "—"
    68 def money(node, path):
    69   matches = nodes(node, path)
    70   if not matches or not matches[0].text:
    71     return "—"
    72   amount = matches[0]
    73   return f"{amount.get('currencyID', '?')} {amount.text}"

Kampfansage an Datenwust

Nun ist die Liste mit den Waren aus meinem persönlichen Garagenverkauf nur etwa ein Dutzend Einträge lang, aber ein professioneller Händler könnte auf Ebay Massen an Listings laufen haben. Die API bekämpft diesen Datenwust, indem sie eine vorgegebene Höchstzahl an Treffern pro Aufruf zurückliefert, aber anzeigt, ob es noch weitere gibt. Mit dieser Paginierung kann der Client die Daten häppchenweise abholen, und der Ebay-Server verprasst nicht Unmengen an Rechenpower und Speicher. Die Funktion pages() ab Zeile 21 in Listing 2 setzt die Maximalzahl einzuholender Seiten auf 1001, ruft den verlangten API-Endpunkt auf, und sieht nach, ob das Ergebnis weitere Trefferseiten ankündigt.

Die Anzeige im Terminal listet später Käufe und Verkäufe, sowie noch laufende Listings auf. Letzere holt der API-Aufruf GetMyeBaySelling ein, während GetOrders abgeschlossene Transaktionen liefert. Dabei kann es sich sowohl um Käufe als auch Verkäufe handeln. Beide lassen sich unterscheiden anhand der OrderRole, die, wenn sie auf "Seller" gesetzt ist, Verkäufe liefert, und als "Buyer" Einkäufe.

Für jede Rolle liefert pages() ab Zeile 46 einen Array von Ebay-Bestellungen zurück, den sorted() mit der eingestellten Lambda-Funktion in umgekehrter zeitlicher Reihenfolge sortiert. Analoges gilt für die offenen Listings in listings, die umgekehrt nach ihrer eingestellten Ablaufzeit sortiert werden, bevor fetch() alle eingeholten Daten, also Username, Listings und Bestellungen in Zeile 53 an den Aufrufer zurückreicht.

Vorsichtig Addieren

Die Ebay-Übersicht zeigt später auch aufsummierte Geldbeträge an, wie zum Beispiel alle Einnahmen aus Verkäufen oder Gesamtkosten aller Bestellungen. Wie schon vor zehn Jahren im Snapshot präsentiert, kann selbst Ebay nicht richtig addieren, und verrechnet sich bis zum heutigen Tag regelmäßig um Cent-Beträge ([2]). Richtig geht's in Python mit dem Decimal-Paket, das Geldbeträge in Stringform (zum Beispiel "1.23", aber nicht "1,23") einliest. Es umschifft den Anfängerfehler, Geldbeträge als ungenaue Fließkommazahlen zu speichern und addiert korrekt. Zeile 62 in Listing 2 legt mit defaultdict ein Dictionary an, also eine Hashmap, die, wie in Python üblich, nebenbei auch noch die Reihenfolge neuer Einträge im Gedächtnis behält, aber auch Nullwerte für noch nicht existierende Einträge bereithält. So kann sums["USD"] += Decimal(text) in Zeile 66 einfach losaddieren, ohne zu prüfen, ob der Eintrag im Hash sums bereits existiert.

Soweit das Einholen und die Verarbeitung der Daten, nun macht sich das Hauptprogramm main() ab Zeile 21 in Listing 3 daran, das komplette Dashboard nach Abbildung anzuzeigen.

Listing 3: ebay_dashboard.py

    01 import argparse
    02 from datetime import datetime
    03 from pathlib import Path
    04 import yaml
    05 from tabulate import tabulate
    06 from ebay_api import Ebay
    07 from ebay_data import fetch, money, nodes, totals, unshipped, value
    08 def clean(text):
    09   return "".join(c if c.isprintable() else " " for c in str(text))
    10 def date(text):
    11   if not text:
    12     return "—"
    13   return datetime.fromisoformat(text.replace("Z", "+00:00")).astimezone(
    14     ).strftime("%b %d %H:%M")
    15 def table(title, headers, rows, limit):
    16   print(f"\n{title} ({len(rows)})")
    17   cells = [[clean(c) for c in row] for row in rows[:limit or None]]
    18   print(tabulate(cells, headers, disable_numparse=True) or "No entries.")
    19   if limit and len(rows) > limit:
    20     print(f"Showing {limit} of {len(rows)}; use -limit 0 for all.")
    21 def main():
    22   parser = argparse.ArgumentParser(description=__doc__)
    23   parser.add_argument("-config", default="ebay.yaml")
    24   parser.add_argument("-days", type=int, default=30)
    25   parser.add_argument("-limit", type=int, default=20)
    26   args = parser.parse_args()
    27   if not 1 <= args.days <= 90 or args.limit < 0:
    28     parser.error("-days must be 1–90; -limit must be nonnegative")
    29   with open(args.config) as source:
    30     config = yaml.safe_load(source)
    31   user, listings, sales, purchases = fetch(Ebay(config), args.days)
    32   print("EBAY TRADING DASHBOARD —", clean(value(user, "UserID")))
    33   print(f"Last {args.days} days | Feedback: {value(user, 'FeedbackScore')}")
    34   print("Paid sales:", totals(sales), "| Paid purchases:", totals(purchases))
    35   print("Active listings:", len(listings), "| Awaiting shipment:",
    36      sum(unshipped(o) for o in sales))
    37   for title, orders, partner in (("SALES", sales, "BuyerUserID"),
    38                  ("PURCHASES", purchases, "SellerUserID")):
    39     rows = []
    40     for o in orders:
    41       titles = "; ".join(value(t, "Item/Title") + " ×" +
    42                value(t, "QuantityPurchased") for t in
    43                nodes(o, "TransactionArray/Transaction"))
    44       state = " / no shipment recorded" if unshipped(o) else ""
    45       rows.append([date(value(o, "CreatedTime")), value(o, "OrderID"),
    46             clean(titles)[:52],
    47             value(o, partner), money(o, "Total"),
    48             value(o, "OrderStatus") + state])
    49     table(title, ["DATE", "ORDER", "ITEMS", "PARTNER", "TOTAL", "STATUS"],
    50        rows, args.limit)
    51   rows = [[value(i, "ItemID"), clean(value(i, "Title"))[:52],
    52       money(i, "SellingStatus/CurrentPrice"),
    53       max(0, int(value(i, "Quantity", "0")) -
    54         int(value(i, "SellingStatus/QuantitySold", "0"))),
    55       value(i, "SellingStatus/BidCount", "0"),
    56       value(i, "WatchCount", "0"),
    57       date(value(i, "ListingDetails/EndTime"))] for i in listings]
    58   table("ACTIVE LISTINGS", ["ITEM", "TITLE", "PRICE", "QTY", "BIDS",
    59                "WATCHERS", "ENDS"], rows, args.limit)
    60 if __name__ == "__main__":
    61   try:
    62     main()
    63   except RuntimeError as error:
    64     raise SystemExit(str(error)) from None
    65   except Exception as error:
    66     raise SystemExit(f"{type(error).__name__}: {error}") from None

Dazu definiert es mit table() ab Zeile 15 einen Tabellenformatierer, der wiederum auf Pythons Paket tabulate zurückgreift. Das Hauptprogramm main() ab Zeile 21 analysiert zunächst die Kommandozeilenoptionen, denn der User kann den Namen der Konfigurationsdatei mit dem Refresh-Token mit -config einstellen, sowie die Anzahl der Tage, die die API-Abfrage zurückreicht (-days) und die Maximalzahl der dargestellten Einträge (-limit).

Der Aufruf von fetch() in Zeile 31 holt mit insgesamt drei API-Abfragen (oder sogar mehr, falls die Paginierung anspringt) alle erforderlichen Daten vom Ebay-Server und liefert sie in vier Variablen (user, listings, sales, purchases) zurück. Nach der Ausgabe der Header-Zeilen mit Usernamen und Gesamtbeträgen iteriert die große for-Schleife ab Zeile 37 über alle Verkäufe und Bestellungen und gibt jeweils eine Tabelle mit Transaktionen aus.

Ab Zeile 51 geht es dann an die aktiven Listings mit Waren, die der User zur Zeit auf Ebay anbietet. Die Tabelle ab Zeile 58 druckt zu jedem Angebot Details als Tabellenzeile aus. Da Python bei auftretenden Fehlern Exceptions wirft, die die App nicht direkt vor Ort verarbeitet, sondern hochblubbern lässt, bis das Hauptprogramm sie ab Zeile 63 auffängt, fügt Zeile 66 noch den Fehlerstring zur Meldung hinzu, damit der Aufrufer weiß, wo's hängt.

Abhängigkeiten zur Laufzeit

Golang holt ja bekanntlich einzubindende Pakete zur Compile-Zeit herein und wickelt sie zusammen mit der Applikation in ein Binary. Python hingegen wirft zuerst das fertige Skript an, und sucht dann einzubindende Pakete zusammen, die zu diesem Zeitpunkt hoffentlich auf dem Laufzeitsystem vorliegen.

Listing 4: requirements.txt

    1 requests>=2.32,<3
    2 PyYAML>=6,<7
    3 tabulate>=0.9,<1
    4 lxml>=5,<7

Dieser Ansatz führt erfahrungsgemäß zur sogenannten "Dependency Hell", denn verschiedene Applikationen benötigen unter Umständen bestimmte Versionen der externen Pakete, und so hat es sich eingebürgert, Python-Apps in virtuellen Umgebungen zu starten. Listing 4 führt die Liste mit allen zu installierenden Paketen und der folgende Dreisprung legt eine virtuelle Umgebung an, installiert die Pakete aus requirements.txt (Listing 4) dort und ruft dann das Ebay-Dashboard auf:

$ python3 -m venv .venv
$ .venv/bin/python -m pip install -r requirements.txt
$ .venv/bin/python ebay_dashboard.py

Dabei müssen die Listings 1 und 2 ebenfalls im aktuellen Verzeichnis liegen, da das Hauptprogramm in Listing 3 sie dort sucht. Dann sollte ebay.yaml noch die Credentials des Developer-Accounts von der Registrierungsseite enthalten, sowie nach dem oben durchschrittenen Token-Tanz einen Refresh-Token. Dann kann es losgehen, die App kontaktiert den Ebay-Server, bekommt einen Access-Token, holt dann die Daten ein und stellt sie strukturiert im Terminal als Übersicht dar. Der Fantasie sind dabei keine Grenzen gesetzt, man könnte sogar eine eigene Trading-Plattform schreiben und damit Handel treiben!

Infos

[1]

Listings zu diesem Artikel: http://www.linux-magazin.de/static/listings/magazin/2026/12/snapshot/

[2]

Michael Schilli, "Ladenhüter", Linux-Magazin 06/2016, https://www.linux-magazin.de/ausgaben/2016/06/perl-snapshot/

Michael Schilli

arbeitet als Software-Engineer in der San Francisco Bay Area in Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen verschiedener Programmiersprachen. Unter mschilli@perlmeister.com beantwortet er gerne Ihre Fragen.