PharmaAutopilot
...

Python SDK

Package: msv3

Das Python SDK bietet synchronen Zugriff auf das MSV3 Smart Gateway ab Python 3.10+.


Installation

pip install msv3

Erfordert Python 3.10+. Das SDK hat keine Drittanbieter-Abhaengigkeiten -- nur die Standardbibliothek.


Grundeinrichtung

from msv3 import Msv3Client

client = Msv3Client(
    api_key="pk_live_abc123",   # oder pk_test_... fuer Sandbox
)
ParameterTypStandardBeschreibung
api_keystrerforderlichIhr Gateway-API-Schluessel
base_urlstrhttps://api.msv3gateway.example.comUeberschreibung fuer Tests
timeoutfloat30.0Anfrage-Timeout in Sekunden

Zugangsdaten

MSV3-Zugangsdaten werden pro Aufruf uebergeben, nie im Client gespeichert:

from msv3 import Msv3Credentials

creds = Msv3Credentials(
    wholesaler="noweda",
    username="Now00079800",
    password="your-password",
)

Ressourcen

RessourceAttributBeschreibung
ConnectionResourceclient.connectionGrosshaendler-Konnektivitaet testen
WholesalersResourceclient.wholesalersGrosshaendler auflisten und registrieren
AvailabilityResourceclient.availabilityEchtzeit-Bestandsabfragen
OrdersResourceclient.ordersBestellungen aufgeben und verfolgen
ContractsResourceclient.contractsVertragsdaten und Lieferfenster
DeliveriesResourceclient.deliveriesLieferbenachrichtigungen
ReturnsResourceclient.returnsRetouren-Autorisierung und Ankuendigungen
DocumentsResourceclient.documentsPDF-Dokumente herunterladen
WebhooksResourceclient.webhooksWebhook-Abonnement-Verwaltung

Verbindung testen

result = client.connection.test(creds)
print(f"Verbunden mit {result.wholesaler} in {result.latency_ms}ms")

Verfuegbarkeit

client.availability.check(creds, items)

result = client.availability.check(creds, items=[
    {"pzn": "761271", "quantity": 5},
    {"pzn": "10203595", "quantity": 1, "demand_type": "direct"},
])

for item in result.items:
    if item.available:
        d = item.deliveries[0]
        print(f"PZN {item.pzn}: {d.quantity} Stueck, ETA {d.estimated_at}")
    else:
        print(f"PZN {item.pzn}: nicht verfuegbar")

    if item.substitution:
        print(f"  Ersetzt durch PZN {item.substitution.replacement_pzn}")

client.availability.bulk(creds, pzns)

result = client.availability.bulk(creds, pzns=[
    "761271", "4211896", "10203595", "99999999"
])
print("Auf Lager:", result.available_pzns)

Bestellungen

client.orders.create(creds, items, *, dry_run=False)

order = client.orders.create(creds, items=[
    {"pzn": "761271", "quantity": 3, "delivery_preference": "backorder"},
    {"pzn": "10203595", "quantity": 1},
])

print(f"Status: {order.status}")          # 'confirmed'
print(f"Request ID: {order.request_id}")

for item in order.items:
    print(f"PZN {item.pzn}: {item.quantity_confirmed}/{item.quantity_ordered}")
    for d in item.deliveries:
        print(f"  {d.type}: {d.quantity} Stueck, Tour {d.tour}")

Liefervorgaben: "normal" (Standard), "backorder", "grouped", "disposition"

Dry Run

order = client.orders.create(creds, items=[...], dry_run=True)
# order.status == "dry_run"

Nachtmodus

if order.status == "queued_night_mode":
    print("Grosshaendler im Nachtmodus. Bestellung vorgemerkt.")

client.orders.status(creds, order_id)

status = client.orders.status(creds, order.request_id)

if status.status == "available":
    print(f"Bestellung {status.order_id}: {len(status.items)} Artikel")
elif status.status == "expired":
    print("Bestellantwort nicht mehr verfuegbar")
elif status.status == "unknown":
    print("Grosshaendler kennt diese Bestell-ID nicht")

Lieferungen

response = client.deliveries.list(creds)

for delivery in response.deliveries:
    print(f"Lieferung {delivery.tracking_number} ({delivery.date})")
    for item in delivery.items:
        print(f"  PZN {item.pzn}: {item.quantity_delivered} geliefert")

# Bestaetigen
tracking_numbers = [d.tracking_number for d in response.deliveries]
client.deliveries.confirm(creds, tracking_numbers=tracking_numbers)

Webhooks

from msv3.types import WebhookCreateRequest

webhook = client.webhooks.create(
    WebhookCreateRequest(
        url="https://example.com/hooks/msv3",
        wholesaler="noweda",
        events=["delivery.received", "order.status_changed"],
    )
)

# WICHTIG: signing_secret jetzt speichern!
print(f"Webhook ID: {webhook.id}")
print(f"Secret: {webhook.signing_secret}")

Signatur-Verifizierung

from msv3.webhook_verify import verify_webhook_signature

@app.route("/hooks/msv3", methods=["POST"])
def handle_webhook():
    payload = request.get_data()   # Roh-Bytes - NICHT zuerst parsen
    signature = request.headers.get("X-MSV3-Signature", "")
    secret = os.environ["MSV3_WEBHOOK_SECRET"]

    if not verify_webhook_signature(payload, signature, secret):
        return "Invalid signature", 401

    event = request.get_json()
    print(f"Event: {event['event']} von {event['wholesaler']}")
    return "OK", 200

Fehlerbehandlung

from msv3.errors import (
    Msv3ApiError,
    Msv3AuthError,
    Msv3BadRequestError,
    Msv3ProductUnavailableError,
    Msv3RateLimitError,
    Msv3WholesalerUnavailableError,
)
import time

try:
    order = client.orders.create(creds, items=[...])

except Msv3AuthError as e:
    print(f"Authentifizierung fehlgeschlagen: {e}")

except Msv3BadRequestError as e:
    print(f"Ungueltige Anfrage: {e}")

except Msv3ProductUnavailableError as e:
    print(f"Bestellung abgelehnt (Code {e.code}): {e}")

except Msv3RateLimitError as e:
    wait = e.retry_after or 60
    print(f"Rate-Limit. Warte {wait}s...")
    time.sleep(wait)

except Msv3WholesalerUnavailableError as e:
    print(f"Grosshaendler {e.wholesaler} nicht erreichbar: {e}")

except Msv3ApiError as e:
    print(f"API-Fehler {e.status} [{e.type}]: {e}")
AttributTypBeschreibung
statusintHTTP-Statuscode
typestrMaschinenlesbarer Fehlercode
messagestrMenschenlesbare Beschreibung
wholesalerstr | NoneGrosshaendler-ID
codestr | NoneRoher MSV3-Fehlercode
retry_afterint | NoneWartezeit in Sekunden (nur 429)
request_idstrRequest-ID aus dem X-Request-Id-Header

Retry-Strategie

Das SDK wiederholt nicht automatisch. Implementieren Sie Retry-Logik in Ihrer Anwendung:

import time
from msv3.errors import Msv3WholesalerUnavailableError

def create_order_with_retry(client, creds, items, max_attempts=3):
    for attempt in range(1, max_attempts + 1):
        try:
            return client.orders.create(creds, items=items)
        except Msv3WholesalerUnavailableError as e:
            if attempt < max_attempts:
                delay = 2 ** (attempt - 1)
                print(f"Versuch {attempt} fehlgeschlagen, erneut in {delay}s...")
                time.sleep(delay)
            else:
                raise

Vollstaendiges Beispiel

import os
from msv3 import Msv3Client, Msv3Credentials
from msv3.errors import Msv3ApiError

client = Msv3Client(api_key=os.environ["MSV3_API_KEY"])

creds = Msv3Credentials(
    wholesaler="noweda",
    username=os.environ["MSV3_USERNAME"],
    password=os.environ["MSV3_PASSWORD"],
)

try:
    # 1. Verbindung testen
    connection = client.connection.test(creds)
    print(f"Verbunden ({connection.latency_ms}ms)")

    # 2. Verfuegbarkeit pruefen
    availability = client.availability.check(creds, items=[
        {"pzn": "761271", "quantity": 3},
    ])
    if not availability.items[0].available:
        print("PZN 761271 nicht verfuegbar")
        raise SystemExit(1)

    # 3. Bestellung mit Nachlieferung aufgeben
    order = client.orders.create(creds, items=[
        {"pzn": "761271", "quantity": 3, "delivery_preference": "backorder"},
    ])
    print(f"Bestellstatus: {order.status}")
    print(f"Request ID: {order.request_id}")

except Msv3ApiError as e:
    print(f"MSV3-Fehler {e.status} [{e.type}]: {e}")
    raise SystemExit(1)