weclappy 1.0: Der Python-Client, der die weclapp-API wirklich versteht
Seit heute liegt weclappy 1.0.1 auf PyPI. Es ist das erste stabile Release unseres Python-Clients für die weclapp REST API, und es ist mehr als eine Versionsnummer: weclappy implementiert den kompletten Load-Management-Vertrag von weclapp, wiederholt keinen Write, für den es nicht bürgen kann, und macht aus den Eigenheiten der API v2 (Zusatzfelder, referencedEntities, additionalProperties, Paginierung) ganz normale Python-Objekte.
Dieser Artikel ist bewusst lang. Er soll das Nachschlagewerk sein, das wir uns selbst gewünscht hätten, als wir die ersten weclapp-Integrationen in Python gebaut haben. Du findest hier die Kurzfassung der Doku, Praxisbeispiele aus unserem Alltag bei wals.pro und Rechenbeispiele, die zeigen, warum die einzelnen Entscheidungen Sinn ergeben.
- Installation:
pip install weclappy - Quellcode und Doku: github.com/Wals-pro/weclappy
- Paket: pypi.org/project/weclappy
- Änderungen: CHANGELOG
- Load-Management im Detail: docs/load-management.md
- Offizielle weclapp-API-Doku: weclapp.com/api2
weclappy ist unabhängig und community-maintained, kein Produkt von weclapp. Wir bei wals.pro bauen Fulfillment-Anbindungen, Integrationen und KI-Werkzeuge rund um weclapp. Die Library ist das Stück Infrastruktur, auf dem das alles läuft.
Fünf Zeilen, die alles zeigen
from weclappy import Weclapp
client = Weclapp.for_tenant("acme", api_key)
orders = client.get_all(
"salesOrder",
{"status-eq": "ORDER_CONFIRMED", "includeReferencedEntities": "customerId"},
)
print(orders[0].customer.name) # customerId wird zur referenzierten Partei aufgelöst
Hinter diesen fünf Zeilen passiert: sort=id wird gesetzt, Seite 1 wird gelesen, bei Bedarf /count abgefragt, die restlichen Seiten laufen parallel unter einem adaptiven Controller, Duplikate und Lücken werden erkannt, und orders[0].customer findet sich über referencedEntities selbst. Du schreibst nur die Geschäftslogik.
Warum Python, und warum kein eigener Client mehr
Bevor wir in die Technik gehen, ein Wort zu den Werten hinter weclappy, denn sie erklären die meisten Designentscheidungen.
Python ist schnell gebaut und skaliert trotzdem. Ein Skript, das heute Nachmittag Artikelpreise abgleicht, kann nächste Woche eine FastAPI-Anwendung mit Webhook-Endpunkten sein, ohne die Sprache oder den Client zu wechseln. Wir betreiben weclappy in Cloud Run, in Containern und in schlichten Cron-Jobs. Ein pip install, ein Container, fertig. Kein Build-Schritt, kein generierter Code, kein SDK-Generator, dessen Output du nach jedem API-Update neu prüfen musst.
Leichtgewichtig und dokumentiert. Zwei Laufzeitabhängigkeiten (requests, urllib3), Python 3.12+, py.typed, mypy strict, 97 % Testabdeckung inklusive eines In-Process-Fake-Servers, der Queueing, 429 und abgebrochene Verbindungen simuliert. Die README ist die Doku, und sie beschreibt jede Entscheidung mit Begründung.
Gebaut für die Zusammenarbeit mit KI. Nach unserer Erfahrung schreiben KI-Assistenten in Python den verlässlichsten Integrationscode, und ein typisierter, vollständig dokumentierter Client macht daraus einen echten Hebel: Du gibst dem Assistenten die README mit, und er nutzt get_all, iter_keyset oder to_payload() korrekt, statt sich eigene Paginierung auszudenken. Wir erleben das täglich, weil unser weclapp-MCP-Server auf genau dieser Library sitzt.
Die weclapp-API wird vollständig und sauber unterstützt. Offizielle Endpunkte, Zusatzfelder in allen elf Werttypen, referencedEntities, additionalProperties, Dateiupload und Download, Methodenaufrufe wie createSalesInvoice, dazu die inoffiziellen Endpunkte klar gekennzeichnet mit Fallback.
Und daraus folgt der eigentliche Punkt: Es lohnt sich nicht mehr, selbst einen Client zu bauen. Nicht, weil es schwer wäre, einen HTTP-Request an weclapp zu schicken. Sondern weil ein Client, der im Produktivbetrieb standhält, eine lange Liste an Verhalten braucht, die man erst nach dem dritten Vorfall versteht.

Wie weclapp Last steuert, und was das für deinen Code heißt
Das Wichtigste zuerst, weil es die meisten Integrationsprobleme erklärt: weclapp hat kein Rate-Limit pro Minute. weclapp begrenzt die Anzahl gleichzeitig aktiver Requests pro Mandant. Die Zahl ist nicht veröffentlicht und kann je Mandant und über die Zeit variieren. Requests oberhalb dieser Grenze landen in einer Queue, aktuell bis zu etwa 30 Sekunden, danach antwortet weclapp mit HTTP 429.
Last wird als Request-Zeit abgerechnet, nicht als Request-Anzahl. Wenige große Requests sind günstiger als viele kleine.
Jede Antwort auf einen Request, der in der Queue gewartet hat, trägt zwei Header:
| Header | Bedeutung |
|---|---|
X-Weclapp-Wait-Ms |
Zeit, die der Request in weclapps Queue verbracht hat. Ein Bericht, keine Anweisung. |
X-Weclapp-Wait-Reason |
concurrency (zu viele Requests aktiv), load (Gesamtlast zu hoch) oder beides. Erscheint auf 2xx und 429. |
Und zwei Header kannst du selbst setzen:
| Header | Bedeutung |
|---|---|
X-Weclapp-Wait-Timeout-Ms |
Längste Queue-Wartezeit, die du akzeptierst. Kann den Server-Default nur senken. |
X-Weclapp-Request-Timeout-Ms |
Längste serverseitige Verarbeitungszeit. Darüber liefert weclapp ein 400 request_timeout. |
weclapp bittet Clients ausdrücklich darum, proaktiv zu drosseln, sobald Wartezeiten steigen, statt auf 429 zu warten, nach 429 exponentiell zurückzugehen und nur sichere Methoden automatisch zu wiederholen. Genau das tut weclappy.
Rechenbeispiel: Warum „einfach parallel" nach hinten losgeht
Nimm einen Mandanten mit einer (angenommenen, unveröffentlichten) Grenze von 5 gleichzeitigen Requests und Requests, die je 2 Sekunden Verarbeitung brauchen.
| Dein Client schickt gleichzeitig | Letzter Request wartet in der Queue | Ergebnis |
|---|---|---|
| 5 | 0 s | alles sofort |
| 25 | 4 Wellen × 2 s = 8 s | geht durch, aber jede Antwort meldet concurrency
|
| 100 | 19 Wellen × 2 s = 38 s | ab etwa 30 s: HTTP 429 für den Rest |
Ein naiver Thread-Pool mit 100 Workern ist also nicht schneller als einer mit 5, er produziert nur Queue-Zeit und 429-Antworten. Und das Schlimmste: Jeder 429 nach einer vollen Queue hat den Mandanten bereits 30 Sekunden belegt. Die Grenze kennst du nicht, sie ändert sich, und sie wird von allen Integrationen des Mandanten geteilt. Der einzige stabile Weg ist, die Header zu lesen und die eigene Parallelität daran auszurichten.
Adaptive Parallelität: der AIMD-Controller
weclappy hält pro Client (oder pro Mandant, wenn du den Controller teilst) ein Target: die Anzahl paralleler Reads, die gerade erlaubt sind. Jeder Read holt sich vor dem Senden ein Permit und gibt es mit der Antwort zurück. Das Target wird einmal pro Epoche angepasst, wobei eine Epoche target abgeschlossene Antworten lang ist.
| Ereignis | Wirkung auf das Target |
|---|---|
| Start | 2 |
| Obergrenze | 10 (Weclapp(max_concurrency=...)) |
Wait-Reason concurrency oder Wartezeit ≥ 250 ms |
−1 (höchstens eine Senkung pro Epoche) |
Wait-Reason load oder Wartezeit ≥ 2.000 ms |
halbiert, aufgerundet (höchstens eine Senkung pro Epoche) |
| HTTP 429 | 1, plus gemeinsamer Cooldown von mindestens 2 s, in dem kein Request rausgeht |
| 5xx oder Transportfehler | keine Änderung, aber kein Wachstum in dieser Epoche |
| Volle Epoche, Fenster ausgelastet, keine Senkung, kein Fehler | +1 |

Zwei Details in dieser Grafik sind der Unterschied zwischen einem Controller, der im Alltag funktioniert, und einem, der nervt:
Höchstens eine Senkung pro Epoche. Antworten kommen in Bursts. Wenn vier gleichzeitig laufende Reads alle load melden, beschreiben sie denselben Moment. Ein Controller, der pro Antwort halbiert, würde in einer einzigen Runde von 10 auf 1 fallen (10 → 5 → 3 → 2 → 1). weclappy reagiert einmal, beobachtet die Wirkung, und reagiert dann erneut, falls nötig. In der Grafik siehst du das bei „4× Wait-Reason: concurrency → nur −1".
Wachstum nur bei Sättigung. Das Target wächst nur, wenn der Caller die Slots tatsächlich genutzt hat. Ein sequenzieller Aufrufer treibt das Target nicht hoch, und nach einem 429 erholt sich der Controller wieder, statt für immer bei 1 zu bleiben (ein Fehler, den unser interner 0.7.0-Branch hatte und der nie veröffentlicht wurde).
Writes halten kein Permit, warten aber auf einen aktiven 429-Cooldown. Ein Request in eine Queue, die gerade 429 geantwortet hat, bekommt nur den nächsten 429.
Einen Controller pro Mandant teilen
Die Grenze gilt pro Mandant, nicht pro Prozess. Wenn mehrere Clients (etwa ein Webhook-Handler und ein Sync-Job im selben Prozess) denselben Mandanten bedienen, teilen sie sich einen Controller:
from weclappy import ConcurrencyController, ConcurrencySettings, Weclapp
shared = ConcurrencyController(ConcurrencySettings(max_concurrency=8))
sync = Weclapp.for_tenant("acme", api_key, concurrency=shared)
webhooks = Weclapp.for_tenant("acme", api_key, concurrency=shared)
Das zahlt sich doppelt aus: Die Clients konkurrieren nicht gegeneinander um die Queue, und ein warmer Controller startet nicht jedes Mal wieder bei 2 (siehe Rechenbeispiel im nächsten Abschnitt).
Paginierung, der du vertrauen kannst
get_all ist die Methode, die du am häufigsten nutzt, und sie macht in 1.0 mit dem Default threaded="auto" Folgendes:
- Wenn
paramswedersortnochorderByenthält, setzt weclappysort=id. Offset-Seiten ohne stabile Reihenfolge verschieben Zeilen über Seitengrenzen. - Seite 1 wird sequenziell gelesen.
- Ist Seite 1 kurz, ist der Read fertig: kein Count, kein Thread-Pool, genau ein Request. Die meisten kleinen Reads kosten exakt einen Request.
- Sonst fragt weclappy
GET {entity}/countmit dem Filterteil der Parameter. Übersteigt der Countmax_records, bricht der Read ab, bevor eine weitere Seite gelesen wird. - Die Seiten 2 bis N laufen parallel unter dem Controller, höchstens
min(max_workers, target)gleichzeitig. - Die Seiten werden in Seitenreihenfolge zusammengeführt. Eine doppelte Id über Seiten hinweg oder weniger Zeilen als gezählt wirft
WeclappPaginationError: Der Datenbestand hat sich während des Reads geändert, du solltest den Read wiederholen.
Rechenbeispiel: 50.000 Aufträge

Das ist eine Modellrechnung, keine Messung, aber sie rechnet exakt mit den Regeln der Library. Annahmen: 50 Seiten à 1.000 Zeilen, 0,8 s pro Seite, 0,3 s für /count, keine Queue-Wartezeit.
| Variante | Rechnung | Dauer |
|---|---|---|
| weclappy 0.6, sequenziell | 50 × 0,8 s | 40,0 s |
| weclappy 1.0, kalter Controller | Seite 1 (0,8 s) + Count (0,3 s) + Wellen mit 2, 3, 4, … 10 parallelen Seiten (9 × 0,8 s) | 8,3 s |
| weclappy 1.0, warmer geteilter Controller | Seite 1 (0,8 s) + Count (0,3 s) + 49 Seiten in 5 Wellen zu 10 (5 × 0,8 s) | 5,1 s |
Drei Dinge fallen auf. Erstens: Der Faktor zwischen sequenziell und parallel liegt bei knapp fünf bis acht, nicht bei zehn, obwohl die Obergrenze 10 ist. Der Controller tastet sich hoch, und das ist Absicht: Er weiß nicht, welche Grenze der Mandant gerade hat. Zweitens: Ein geteilter, bereits warmer Controller spart nochmals über drei Sekunden, weil die Anlaufphase entfällt. Drittens: Alle drei Varianten erzeugen dieselbe Request-Zeit auf dem Server. Parallelität verkürzt deine Wartezeit, sie senkt nicht die Last. Dafür brauchst du den nächsten Abschnitt.
Rechenbeispiel: 3.721 Artikel, drei Zugriffsmuster

Die Zahl auf der rechten Kachel ist gemessen: 3.721 Artikel in 0,6 s mit 5 Requests auf der weclapp-Sandbox (API v2, Oktober 2026). Vier Seiten à 1.000 plus ein /count, parallel gelesen.
Die linke Kachel ist das Muster, das wir in handgeschriebenen Clients am häufigsten sehen: eine Id-Liste aus einem anderen System, dann ein GET article/id/{id} pro Id. 3.721 Requests. Jeder einzelne belegt einen Slot in der Mandanten-Queue, jeder einzelne zählt als Request-Zeit.
Die Mitte ist get_by_ids: Es liest die Zeilen mit id-in=[...] in parallelen Chunks. Die Chunk-Größe kommt aus Sandbox-Messungen: Die Edge vor weclapp (Akamai) lehnt URLs ab etwa 8,9 KB mit HTTP 400 ab, 640 numerische Ids passen darunter, 500 lassen Reserve für längere Ids und weitere Parameter. 3.721 ÷ 500 ergibt 8 Chunks.
rows = client.get_by_ids("article", ids, {"properties": "id,articleNumber,name"})
Ein Detail, das dich sonst einen Nachmittag kostet: id-in wird paginiert wie jede andere Liste. Ohne explizites pageSize bekommst du nur die Default-Seitengröße an Treffern zurück. weclappy schickt pro Chunk pageSize=len(chunk).
Für große Projektionen (viele Felder, Positionen, Referenzen) gibt es strategy="ids", das Muster, das weclapp selbst empfiehlt: erst nur die Ids mit allen Filtern lesen, dann die Zeilen mit der vollen Projektion über get_by_ids.
orders = client.get_all(
"salesOrder",
{
"status-eq": "ORDER_CONFIRMED",
"properties": "id,orderNumber,customerId,orderItems",
"includeReferencedEntities": "customerId",
},
strategy="ids",
)
Exporte, die keine Zeile verlieren: iter_keyset
Offset-Paginierung ist kein Snapshot. Wird während eines langen Exports ein Datensatz angelegt oder gelöscht, verschieben sich die Seitengrenzen. weclappy erkennt Duplikate und Lücken, repariert sie aber nicht. Für Exporte und Reports gibt es deshalb Keyset-Paginierung mit sort=id und id-gt=<letzte Id>:
for party in client.iter_keyset("party", {"properties": "id,company", "partyType-eq": "CUSTOMER"}):
export(party)
Keyset-Iteration ist immun gegen Inserts und Deletes während des Reads, und mit start_after="<id>" setzt du einen abgebrochenen Export genau dort fort, wo er stand. Das ist der Iterator für den nächtlichen Abgleich ins Data Warehouse.
max_records als Notbremse
client.get_all("article", {"active-eq": "true"}, max_records=50_000)
Ein Filter, der durch einen Tippfehler plötzlich alles matcht, wird vor dem ersten parallelen Request gestoppt, nicht nach einer Minute Volllast auf dem Mandanten.
Retries, die zur API passen, und Writes, die nie doppelt laufen
In 0.6.0 hing ein urllib3-Retry-Adapter am Client, der POST, PUT und DELETE bei 500, 502, 503, 504 und 429 wiederholte. Das klingt harmlos und ist es nicht: Ein 5xx oder ein 429 nach Queue-Wartezeit beweist nicht, dass der Write nicht verarbeitet wurde. weclapp kann die Rechnung längst gebucht haben, bevor die Antwort verloren ging. Ein wiederholtes POST salesInvoice erzeugt dann eine zweite Rechnung, eine wiederholte Lagerbuchung bucht doppelt.
In 1.0 gibt es genau eine Regel: Ein Write wird nur wiederholt, wenn die Verbindung nachweislich nie zustande kam.

| Bedingung | Reads | Writes |
|---|---|---|
| Verbindung nie zustande gekommen (DNS, refused, connect timeout) | Retry, Transient-Budget | Retry, Transient-Budget |
| Ausgang unbekannt (Read-Timeout, Verbindung nach dem Senden weg) | Retry |
nie; WeclappTransportError mit outcome_unknown=True
|
| 500, 502, 503, 504 | Retry, Transient-Budget | nie |
| 429 | Retry, Rate-Limit-Budget, startet den Cooldown | nie (wartet aber auf den Cooldown) |
400 request_timeout, 409 persistence
|
Retry, Problem-Budget | nie |
| 3xx | nie gefolgt; WeclappRedirectError
|
nie gefolgt |
Das Rezept für den unbekannten Ausgang: Lies den Datensatz über einen Business-Key zurück, den du kontrollierst (Kundennummer, externe Referenz, ein Marker im Notizfeld).
import time
from weclappy import WeclappTransportError
def create_party_once(client, customer_number):
try:
return client.post(
"party",
{"partyType": "ORGANIZATION", "company": "Acme", "customerNumber": customer_number},
)
except WeclappTransportError as exc:
if not exc.outcome_unknown:
raise # nachweislich nie gesendet, später gefahrlos wiederholbar
for delay in (0, 2, 5, 10, 20, 30):
time.sleep(delay)
found = client.get("party", params={"customerNumber-eq": customer_number})
if found:
return found[0] # der Write ist durchgegangen
raise # immer noch unbekannt: eskalieren statt erneut schreiben
Zwei Budgets, ein Deckel

| Budget | Default | Wartezeit vor Versuch n (0-basiert) |
|---|---|---|
| Transient (5xx, Netz) | 3 | 0,3 · 2ⁿ s plus bis zu 0,3 s Jitter |
| Rate-Limit (429) | 5 | 2 · 2ⁿ s plus bis zu 2 s Jitter |
Problem (request_timeout, persistence) |
1 | 0,3 · 2ⁿ s plus Jitter |
Jede Wartezeit, auch ein Retry-After vom Server, ist bei max_backoff (60 s) gedeckelt. Der Grund ist ein echter Vorfall aus der Entwicklung: Ein Retry-After: 3600 hat alle Reads eine Stunde blockiert, ein unendlicher Wert hat jeden späteren acquire() mit OverflowError abstürzen lassen. Beides ist in 1.0 geschlossen. Jeder logische Request hat eigene Zähler, ein 429 verbraucht also nicht das Transient-Budget.
Timeouts mit Abstand
| Einstellung | Default | Zweck |
|---|---|---|
| Client-Timeout | 120 s |
requests-Timeout pro Versuch, begrenzt auch das Warten auf ein Permit |
X-Weclapp-Wait-Timeout-Ms |
30.000 | längste akzeptierte Queue-Wartezeit |
X-Weclapp-Request-Timeout-Ms |
110.000 | bewusst unter dem Client-Timeout |
Die 10 Sekunden Abstand sind der Punkt: Ohne sie geben Client und Server im selben Moment auf, und ein Write endet in einem mehrdeutigen Read-Timeout statt in einem definitiven 400 request_timeout. Setzt du pro Request ein kürzeres Timeout, senkt weclappy den Request-Timeout-Header automatisch auf 90 % der Read-Zeit, damit ein ungeduldiger Client keine langlaufenden Ausführungen auf dem Server zurücklässt.
Entities: Zusatzfelder und Referenzen wie normale Felder
Die API v2 liefert Zusatzfelder als Array aus attributeDefinitionId plus typisiertem Wert, Referenzen als separate referencedEntities-Sektion und berechnete Werte als additionalProperties neben dem Ergebnis. Alles korrekt, alles mühsam. weclappy legt das flach auf ein WeclappEntity, eine dict-Unterklasse mit Attributzugriff.

-
Felder als Attribute:
order.orderNumberistorder["orderNumber"]. Dict-Code läuft weiter. -
Zusatzfelder flach: Jedes
customAttributes-Element erscheint als Feld unter seinemattributeKey. Die Definitionen lädt weclappy einmal pro Client, lazy, unter einem Lock, bei der ersten Zeile, die sie braucht. Alle elf Werttypen gehen hin und zurück. -
Referenzen aufgelöst:
order.customerlöstorder.customerIdgegenreferencedEntitiesauf, auch wenn der Bucket anders heißt (customerId→party). Verschachtelt funktioniert das genauso:order.orderItems[0].article.articleNumber. -
additionalProperties gemerged: Pro Zeile als Feld,
entity.additional_propertieslistet sie.
articles = client.get(
"article",
params={
"properties": "id,articleNumber,unitId,unit:id,unit:name",
"additionalProperties": "currentSalesPrice",
"includeReferencedEntities": "unitId",
"pageSize": 5,
},
)
for article in articles:
print(article.articleNumber, article.currentSalesPrice, article.unit.name)
Und der Rückweg ist genauso kurz. to_payload() ist der einzige unterstützte Weg in einen Write: Es verwirft gemergte additionalProperties, baut customAttributes wieder auf, lässt aufgelöste Referenzobjekte weg und konvertiert verschachtelte Entities rekursiv. Die Write-Methoden rufen es für dich auf.
article = client.get("article", "4384", {"properties": "id,version,customAttributes"})
article.lieferfenster = "KW 43" # ein bestehendes, schreibbares Zusatzfeld
client.put("article", article.id, article)
Zwei Sicherheitsregeln, die wir bewusst eingebaut haben: Als readOnly definierte Zusatzfelder werfen lokal, bevor ein Request rausgeht. Und die flache Schnittstelle fügt nie ein Zusatzfeld hinzu, das im Entity fehlt. Neue Attribute schickst du explizit als v2-customAttributes-Element. So kann ein Tippfehler im Attributnamen keine stille Falschbuchung erzeugen.
Optimistic Locking in vier Zeilen
from weclappy import WeclappOptimisticLockError
for _attempt in range(3):
order = client.get("salesOrder", "4384", {"properties": "id,version,commission"})
try:
client.put("salesOrder", order.id, {"version": order.version, "commission": "B-17"})
break
except WeclappOptimisticLockError:
continue # neu lesen, Änderung erneut anwenden
put schickt standardmäßig ignoreMissingProperties=true, ein Teil-Payload ändert also nur die enthaltenen Felder. Für Vollersetzung gibst du {"ignoreMissingProperties": False} mit.
Die versteckten Endpunkte, klar beschriftet
weclapp führt in seinem versteckten OpenAPI-Dokument Endpunkte, die im Alltag enorm helfen, aber keinerlei Kompatibilitätsversprechen tragen. weclappy bietet sie an, markiert sie als inoffiziell und empfiehlt für jeden einen Fallback.
| Methode | Endpunkt | Wofür |
|---|---|---|
query() |
POST {entity}/query |
Filterausdruck im Body, also kein URL-Limit für lange id in [...]-Listen |
query_count() |
POST {entity}/count |
Count mit Body-Filter |
batch_query() |
POST batch/query |
bis zu 500 relative GET-Queries in einem Aufruf |
openapi(include_hidden=True) |
GET meta/openapi.yaml |
das OpenAPI-Dokument deines Mandanten inklusive versteckter Endpunkte |
from weclappy import WeclappAPIError
try:
rows = client.query("article", filter=f"id in [{','.join(ids)}]", properties=["id", "name"], page_size=len(ids))
except WeclappAPIError:
rows = client.get_by_ids("article", ids, {"properties": "id,name"}) # offizieller Fallback
Was wir auf der Sandbox gemessen haben: batch/query akzeptiert höchstens 500 Requests pro Aufruf, die Antwort kommt nicht in Request-Reihenfolge (weclappy sortiert nach Index), und /id/{id}-Pfade werden innerhalb eines Batches abgelehnt. POST {entity}/query sortiert mit orderBy, nicht mit sort. Alle vier zählen als Reads: Sie halten ein Permit und werden wie GET wiederholt.
Observability, ohne Secrets im Log
Jeder physische Versuch, auch ein fehlgeschlagener, ruft deinen on_response-Hook mit einem RequestMetrics-Objekt: Methode, Pfad ohne Query-String, Status, Dauer, Queue-Wartezeit, Wait-Reason, Correlation-Id, Versuchsnummer, Retry-Entscheidung, aktuelles Controller-Target.
from weclappy import RequestMetrics, Weclapp
def record(metrics: RequestMetrics) -> None:
if metrics.wait_ms:
print(f"{metrics.method} {metrics.path} wartete {metrics.wait_ms:.0f} ms ({metrics.wait_reason})")
client = Weclapp.for_tenant("acme", api_key, on_response=record)
client.stats aggregiert alles, inklusive request_seconds, der geschätzten serverseitigen Verarbeitungszeit, also genau der Größe, in der weclapp Last misst. Damit siehst du, welcher Job deinen Mandanten wirklich belastet. Logs und Metriken enthalten nie den API-Key, nie Query-Strings, nie Bodies.
Erweiterungspunkte statt Monkeypatching
import requests
from weclappy import OutgoingRequest, RetryPolicy, Weclapp
def tag(request: OutgoingRequest) -> None:
request.headers["X-Correlation-ID"] = f"sync-job-{request.attempt}"
client = Weclapp.for_tenant(
"acme",
api_key,
session=requests.Session(), # Proxies, Zertifikate, eigene Adapter
before_request=tag,
retry_policy=RetryPolicy(max_retries=2, rate_limit_retries=8, max_backoff=30.0),
)
Dazu request() als öffentliche Ausweichroute für Endpunkte ohne Helfer, mit derselben Lastkontrolle, denselben Retries und demselben Parsing wie jede andere Methode. Die privaten Methoden _send_request und _check_response aus 0.x sind weg, und das ist gut so: Alles, wofür man sie gebraucht hat, geht jetzt über öffentliche, versionierte Schnittstellen.
Sicherheitsleitplanken, die du nicht abschalten musst
-
Redirects werden nie gefolgt. Ein 3xx wirft
WeclappRedirectError. Ein 307/308 kann keinen Write-Body wiederholen, und der API-Key kann keinem Redirect auf einen fremden Host folgen. -
Same-Origin-Guard. Endpunkte sind Pfade relativ zur
base_url. Absolute URLs, fremde Hosts,..-Segmente und Fragmente werfenValueError. -
Pfadsegmente werden encodiert. Eine Id aus einem Webhook-Payload mit
/,?oder#kann keinen anderen Endpunkt erreichen. -
Typisierte Fehler.
WeclappNotFoundError,WeclappValidationError,WeclappOptimisticLockError,WeclappRateLimitError,WeclappPaginationErrorund mehr, alle unterWeclappAPIErrormit dem geparsten Problem-Dokument. Einexcept WeclappAPIErrorfängt weiterhin alles.
Überall hosten, ohne Theater
Weil weclappy nur requests und urllib3 braucht und thread-sicher ist, läuft es dort, wo Python läuft:
- Cloud Run oder ein Container irgendwo: ein Client pro Prozess, geteilt von allen Threads, als Context Manager geschlossen.
- Cloud Functions, Firebase Functions, Lambda, Azure Functions: ein Client auf Modulebene, damit der Connection-Pool und der warme Controller über Aufrufe hinweg erhalten bleiben.
- Webanwendungen mit FastAPI, Flask oder Django: ein Client als Anwendungsressource, die Webhook-Handler und Hintergrundjobs teilen.
-
Cron und Skripte:
pip install weclappy, API-Key aus der Umgebung, fertig.
import os
from weclappy import Weclapp
client = Weclapp.for_tenant(os.environ["WECLAPP_TENANT"], os.environ["WECLAPP_API_KEY"])
def handler(event, context):
open_orders = client.count("salesOrder", {"status-eq": "ORDER_CONFIRMED"})
return {"openOrders": open_orders}
Halte den API-Key immer in der Umgebung oder im Secret Manager deiner Plattform. weclappy sendet ihn als AuthenticationToken und hält ihn aus allen Logs heraus, aber ein exc.response.request.headers enthält ihn weiterhin. Serialisiere keine rohen requests-Objekte in Error-Reporter.
Migration von 0.x
1.0 bricht die API bewusst, einmal und sauber. Die vollständige Liste steht in der README, die wichtigsten Punkte:
- Python ≥ 3.12. 3.9 bis 3.11 werden nicht mehr unterstützt.
-
Paket statt Modul. Importiere öffentliche Namen nur aus
weclappy. -
Keyword-only. Jedes Konstruktor-Argument nach
api_key, inget_allalles nachparams. -
Umbenennungen.
id→entity_id,endpoint→entity. Positionen sind unverändert,id=funktioniert in 1.x noch mitDeprecationWarning. -
get_allist jetzt parallel (threaded="auto"). Für strikt sequenzielle Readsthreaded=False. -
Writes werden nie wiederholt. Behandle
WeclappTransportError.outcome_unknownmit einem Rücklesen. -
Zusätzliche Requests. Ein großes
get_allschickt einen/count, die erste Zeile mit Zusatzfeldern lädtcustomAttributeDefinitioneinmal pro Client. Test-Doubles müssen beides beantworten.
Pinne in Anwendungen weclappy>=1.0,<2. Die öffentliche API ist ab jetzt unter Semantic Versioning eingefroren, Deprecations laufen mindestens ein Minor-Release vor der Entfernung, unterstützt werden die drei neuesten CPython-Versionen.
Was als Nächstes kommt
Auf der Roadmap stehen Bulk-Writes mit derselben Ausgangs-Sicherheit wie Einzel-Writes für 1.1 und eine asynchrone Variante für Version 2. Beides sind Ausblicke, keine Zusagen mit Datum.
Loslegen
python -m pip install weclappy
export WECLAPP_API_KEY="dein-api-key"
- Quellcode, README, Beispiele: github.com/Wals-pro/weclappy
- Paket: pypi.org/project/weclappy
- Load-Management mit allen Sandbox-Messungen: docs/load-management.md
- Offizielle weclapp-API-Dokumentation: weclapp.com/api2
- Fehler melden oder Features vorschlagen: GitHub Issues
- Sicherheitslücken vertraulich melden: SECURITY.md
Wenn du eine weclapp-Integration planst oder eine bestehende stabilisieren willst, die regelmäßig in 429s läuft oder doppelte Belege erzeugt: Lass uns 30 Minuten darüber reden. Wir zeigen dir anhand deiner Jobs, welche Parallelität dein Mandant verträgt, und wo weclappy dir Code abnimmt.
Lizenz: MIT. weclappy ist ein unabhängiges Community-Projekt und steht in keiner Verbindung zu weclapp.