Dataset 137 pesanan dengan per_page 50 berarti satu endpoint dipecah menjadi tiga respons terpisah. Mengambil hanya halaman pertama membuat pipeline kehilangan 87 pesanan tanpa galat apa pun, jadi kesalahan pagination termasuk yang paling berbahaya karena senyap. Pada bagian ini kita membangun penjelajah halaman sebagai generator di src/pagination.py, terpisah dari klien HTTP agar mudah diuji.
Pola Iterator #
Fungsi paginate menerima fungsi fetch_page, bukan objek klien. Pemisahan membuat fungsi ini bisa diuji dengan fetch palsu tanpa server HTTP sama sekali:
def paginate(fetch_page, max_pages=1000):
"""Yield daftar baris per halaman sampai halaman terakhir.
``fetch_page(nomor)`` harus mengembalikan payload berbentuk
``{"data": [...], "meta": {...}}``. Iterasi berhenti ketika halaman kosong,
ketika ``meta.total_pages`` tercapai, atau ketika ``max_pages`` terlampaui
sebagai pagar pengaman kontrak API yang tidak konsisten.
"""
halaman = 1
while halaman <= max_pages:
payload = fetch_page(halaman)
baris = payload.get("data") or []
meta = payload.get("meta") or {}
total_halaman = meta.get("total_pages")
logger.info("halaman %s/%s: %d baris", halaman, total_halaman or "?",
len(baris))
yield baris
if not baris:
return
if total_halaman is not None and halaman >= total_halaman:
return
halaman += 1
raise PaginationError(
f"pagination melebihi {max_pages} halaman; nilai meta.total_pages "
"dari API patut diperiksa"
)Mari kita telaah baris-baris kode di atas:
- Baris 21-22 (
while halaman <= max_pages): Nomor halaman berjalan dari 1; kondisi while menjadi pagar luar dari penjelajahan. - Baris 23-26 (
payload,baris,meta): Respons diurai secara defensif denganor []danor {}, sehingga envelope yang cacat tidak langsung memunculkanTypeError. - Baris 27-28 (
logger.info): Setiap halaman dicatat dengan nomor halaman dan jumlah barisnya; jejak ini tampak pada log run pipeline di Bagian 7. - Baris 29 (
yield baris): Sebagai generator,paginatemenyerahkan satu halaman per iterasi sehingga pemanggil bisa memproses data sejak halaman pertama tiba. - Baris 30-31 (
if not baris: return): Halaman kosong adalah sinyal alami akhir data, terutama ketikametatidak menyertakantotal_pages. - Baris 32-33 (
halaman >= total_halaman): Bilatotal_pagestersedia, penjelajahan berhenti tepat di halaman terakhir tanpa satu permintaan pun yang sia-sia. - Baris 35-38 (
raise PaginationError): Bila keluar dari loop karenamax_pagesterlampaui, penjelajahan dihentikan dengan galat eksplisit, bukan diam-diam berhenti.
Menjelajahi Seluruh Halaman #
Pipeline memakai paginate dengan menggabungkan nomor halaman ke parameter klien. Skrip berikut menghitung seluruh pesanan yang bisa diambil:
python3 - <<'PY'
import logging
from src.client import ApiClient
from src.pagination import paginate
logging.basicConfig(level=logging.INFO, format="%(message)s")
klien = ApiClient("http://127.0.0.1:8765", timeout=5.0)
def fetch(nomor):
return klien.get_json("/api/v1/pesanan", {"page": nomor, "per_page": 50})
total = 0
for baris in paginate(fetch):
total += len(baris)
print("total pesanan:", total)
PYOutput yang Dihasilkan:
halaman 1/3: 50 baris
halaman 2/3: 50 baris
halaman 3/3: 37 baris
total pesanan: 137Tiga baris log menunjukkan penjelajahan berhenti tepat setelah halaman ketiga, dan total 50 + 50 + 37 = 137 cocok dengan total_records pada meta. Ketika kegagalan jaringan terjadi di tengah penjelajahan, klien dari Bagian 2 yang menangani percobaan ulang per halaman, sehingga paginate tetap sederhana.
Pagar Pengaman max_pages #
max_pages melindungi pipeline dari dua skenario buruk: API yang selalu mengklaim ada halaman berikutnya, dan kesalahan logika yang membuat nomor halaman berputar tanpa henti. Tanpa pagar, loop while bisa berjalan selamanya dan menagih kuota API setiap detiknya. Nilai bawaan 1000 halaman diatur di config/pipeline.yaml pada bagian paging.max_pages dan bisa disesuaikan dengan ukuran dataset.
Uji Pagination #
Karena paginate tidak bergantung pada HTTP, tes di tests/test_pagination.py memakai fetch palsu berbasis dict. Dua kasus berikut menguji pagar pengaman dan kondisi meta yang tidak lengkap:
def test_total_hilang_lanjut_sampai_halaman_kosong():
isi = {1: ["a"], 2: ["b"], 3: []}
def fetch(nomor):
return {"data": isi[nomor], "meta": {}}
assert [baris for baris in paginate(fetch)] == [["a"], ["b"], []]
def test_max_pages_memicu_pagar_pengaman():
def fetch(nomor):
return {"data": [f"baris-{nomor}"], "meta": {"total_pages": 10_000}}
with pytest.raises(PaginationError):
list(paginate(fetch, max_pages=5))Tes pertama membuktikan penjelajahan tetap maju walaupun total_pages absen, dengan berhenti pada halaman kosong. Tes kedua memaksa API yang mengklaim 10.000 halaman dan memastikan pagar max_pages=5 menghentikan penjelajahan dengan galat.
python3 -m pytest tests/test_pagination.py -qOutput yang Dihasilkan:
..... [100%]
5 passed in 0.16sPenjelajah halaman kita kini utuh dan teruji, mengikuti pola yang dibahas pada chapter Menangani Pagination API. Namun penjelajahan tiga halaman berarti tiga permintaan beruntun, dan API publik membatasi seberapa rapat permintaan boleh datang. Bagian berikutnya mengajari klien menghormati batas tersebut lewat header Retry-After. Detail tentang perilaku generator dan iterator Python tersedia di dokumentasi tipe bawaan Python
.