# Veröffentlichte Pässe eines Modells

> Listen Sie jede Einheit eines Batteriemodells mit veröffentlichtem Pass auf und geben Sie Pass-ID, aktuelle Version und öffentlichen Link als CSV aus.

Das Skript listet mit [`listBatteries`](https://passport.kabin.sh/docs/de/api-reference/catalog/list-batteries) und den Filtern
`battery_model_id` und `passport_status=published` die Einheiten eines Batteriemodells, deren Pass veröffentlicht
ist, liest jeden Pass mit [`getPassport`](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport) und gibt je Einheit eine
CSV-Zeile aus: Seriennummer, Pass-ID, aktuelle Version, Zeitpunkt der Veröffentlichung und öffentliche Seite. Es
braucht einen Schlüssel mit dem Scope `read`.

- Die ID des Modells liefert [`listBatteryModels`](https://passport.kabin.sh/docs/de/api-reference/catalog/list-battery-models).
- Pässe im Status `update_needed` sind ebenfalls online (ihre aktuelle Version bleibt öffentlich, bis sie erneut
  veröffentlicht werden). Starten Sie das Skript ein zweites Mal mit `passport_status=update_needed`, um sie
  einzuschließen.
- Für eine einzelne Modellversion verwenden Sie `model_version_id` statt `battery_model_id`.

```bash title="cURL"
#!/usr/bin/env bash
# model-passports.sh <model-id>: the published passports of one battery model as CSV (needs curl and jq)
set -euo pipefail
model_id="$1" # the id from listBatteryModels
auth="Authorization: Bearer $BATTERYPASS_API_KEY"
echo "serial_number,passport_id,version_no,published_at,public_url"
cursor=""
while :; do
  page=$(curl -sS --fail --retry 5 -G "$BATTERYPASS_URL/api/v1/batteries" -H "$auth" \
    -d "battery_model_id=$model_id" -d passport_status=published -d limit=200 \
    ${cursor:+--data-urlencode "cursor=$cursor"})
  for id in $(jq -r '.items[].id' <<<"$page"); do
    curl -sS --fail --retry 5 -H "$auth" "$BATTERYPASS_URL/api/v1/batteries/$id/passport" |
      jq -r '[.serial_number, .passport_id, .current_version.version_no, .current_version.published_at, .public_url] | @csv'
  done
  cursor=$(jq -r '.next_cursor // empty' <<<"$page")
  [ -n "$cursor" ] || break
done
```

```ts title="TypeScript"
// model-passports.mts: npx tsx model-passports.mts <model-id>
const BASE = `${process.env.BATTERYPASS_URL}/api/v1`;
const headers = { Authorization: `Bearer ${process.env.BATTERYPASS_API_KEY}` };
const modelId = process.argv[2]; // the id from listBatteryModels
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

async function get(path: string) {
  for (let attempt = 1; ; attempt++) {
    const res = await fetch(BASE + path, { headers });
    if (res.ok) return res.json();
    if (res.status !== 429 || attempt === 5) throw new Error(`${path}: ${res.status} ${(await res.json()).code}`);
    await sleep(Number(res.headers.get('Retry-After') ?? 1) * 1000);
  }
}

console.log('serial_number,passport_id,version_no,published_at,public_url');
let cursor: string | null = null;
do {
  const query = new URLSearchParams({ battery_model_id: modelId, passport_status: 'published', limit: '200' });
  if (cursor) query.set('cursor', cursor);
  const page = await get(`/batteries?${query}`);
  for (const unit of page.items) {
    const p = await get(`/batteries/${unit.id}/passport`);
    const v = p.current_version;
    console.log([p.serial_number, p.passport_id, v?.version_no, v?.published_at, p.public_url].join(','));
  }
  cursor = page.next_cursor;
} while (cursor);
```

```python title="Python"
# model_passports.py: python model_passports.py <model-id>
import csv
import os
import sys
import time

import requests

BASE = os.environ["BATTERYPASS_URL"] + "/api/v1"
session = requests.Session()
session.headers["Authorization"] = "Bearer " + os.environ["BATTERYPASS_API_KEY"]
model_id = sys.argv[1]  # the id from listBatteryModels


def get(path, **params):
    for attempt in range(1, 6):
        res = session.get(BASE + path, params=params, timeout=30)
        if res.status_code != 429 or attempt == 5:
            res.raise_for_status()
            return res.json()
        time.sleep(int(res.headers.get("Retry-After", "1")))


out = csv.writer(sys.stdout)
out.writerow(["serial_number", "passport_id", "version_no", "published_at", "public_url"])
cursor = None
while True:
    params = {"battery_model_id": model_id, "passport_status": "published", "limit": 200}
    if cursor:
        params["cursor"] = cursor
    page = get("/batteries", **params)
    for unit in page["items"]:
        p = get(f"/batteries/{unit['id']}/passport")
        v = p.get("current_version", {})
        out.writerow([p["serial_number"], p.get("passport_id"), v.get("version_no"), v.get("published_at"), p.get("public_url")])
    cursor = page["next_cursor"]
    if not cursor:
        break
```

```go title="Go"
// model-passports: go run . <model-id>
package main

import (
	"encoding/csv"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"time"
)

var (
	base = os.Getenv("BATTERYPASS_URL") + "/api/v1"
	key  = os.Getenv("BATTERYPASS_API_KEY")
)

// get decodes the JSON answer of a GET request into out and waits out 429 answers.
func get(path string, out any) error {
	for attempt := 1; ; attempt++ {
		req, err := http.NewRequest(http.MethodGet, base+path, nil)
		if err != nil {
			return err
		}
		req.Header.Set("Authorization", "Bearer "+key)
		res, err := http.DefaultClient.Do(req)
		if err != nil {
			return err
		}
		if res.StatusCode == http.StatusOK {
			defer res.Body.Close()
			return json.NewDecoder(res.Body).Decode(out)
		}
		res.Body.Close()
		if res.StatusCode != http.StatusTooManyRequests || attempt == 5 {
			return fmt.Errorf("%s: %s", path, res.Status)
		}
		wait, _ := strconv.Atoi(res.Header.Get("Retry-After"))
		time.Sleep(time.Duration(max(wait, 1)) * time.Second)
	}
}

type passport struct {
	SerialNumber   string `json:"serial_number"`
	PassportID     string `json:"passport_id"`
	PublicURL      string `json:"public_url"`
	CurrentVersion struct {
		VersionNo   int    `json:"version_no"`
		PublishedAt string `json:"published_at"`
	} `json:"current_version"`
}

func main() {
	modelID := os.Args[1] // the id from listBatteryModels
	out := csv.NewWriter(os.Stdout)
	defer out.Flush()
	_ = out.Write([]string{"serial_number", "passport_id", "version_no", "published_at", "public_url"})

	cursor := ""
	for {
		q := url.Values{"battery_model_id": {modelID}, "passport_status": {"published"}, "limit": {"200"}}
		if cursor != "" {
			q.Set("cursor", cursor)
		}
		var page struct {
			Items []struct {
				ID string `json:"id"`
			} `json:"items"`
			NextCursor *string `json:"next_cursor"`
		}
		if err := get("/batteries?"+q.Encode(), &page); err != nil {
			log.Fatal(err)
		}
		for _, unit := range page.Items {
			var p passport
			if err := get("/batteries/"+unit.ID+"/passport", &p); err != nil {
				log.Fatal(err)
			}
			v := p.CurrentVersion
			_ = out.Write([]string{p.SerialNumber, p.PassportID, strconv.Itoa(v.VersionNo), v.PublishedAt, p.PublicURL})
		}
		if page.NextCursor == nil {
			break
		}
		cursor = *page.NextCursor
	}
}
```

```text title="Ausgabe"
serial_number,passport_id,version_no,published_at,public_url
SV2027-000184,urn:dpp:0192f0c4-7d1e-7a3b-9c55-2f1e8d4a6b10,4,2026-10-24T06:00:03Z,https://batterypass.example.com/pass/0192f0c4-7d1e-7a3b-9c55-2f1e8d4a6b10
SV2027-000185,urn:dpp:0192f0d7-3e2a-7b1c-8d4e-5f6a7b8c9d0e,2,2026-09-30T14:22:10Z,https://batterypass.example.com/pass/0192f0d7-3e2a-7b1c-8d4e-5f6a7b8c9d0e
```

Die Shell-Fassung setzt jedes CSV-Feld in Anführungszeichen (`@csv`), die anderen nur, wo es nötig ist. Jede
Einheit kostet eine Anfrage, und alle Schlüssel Ihrer Organisation teilen sich 600 Anfragen pro Minute. Pass und
Versionen erklärt [Pässe und Versionen](https://passport.kabin.sh/docs/de/concepts/passports).
