Neden önemli?
Anadolu Market'in sipariş sistemi kargo firmasının API'sinden teslimat durumlarını, Ilgaz Bank'ın ödeme API'sinden tahsilat sonuçlarını, merkez bankasının servisinden döviz kurlarını çeker. Modern yazılımda sistemler birbirleriyle çoğunlukla HTTP üzerinden konuşan API'lerle (Application Programming Interface) haberleşir. Bir API istemcisi yazmak kolaydır; dayanıklı bir istemci yazmak ise ayrı bir beceridir: ağ kopar, sunucu yavaşlar, hız sınırına takılırsın, beklenmedik JSON gelir. Bu derste ikisini de öğreneceksin.
Önceki derste HTML kazımayı gördün; API ise verinin makine için hazırlanmış, kurallı halidir. API varsa kazımaya gerek yoktur. Sunucu tarafını (FastAPI ile uç nokta yazmayı) ileri seviyedeki "API ve web" dersinde göreceksin; burada istemci tarafındayız.
HTTP isteğinin anatomisi
POST /v1/siparisler?kaynak=mobil HTTP/1.1 ← metot, yol, sorgu parametresi
Host: api.anadolumarket.example
Authorization: Bearer eyJhbGciOi... ← kimlik doğrulama başlığı
Content-Type: application/json ← gövdenin biçimi
Accept: application/json ← beklediğin yanıt biçimi
{"musteri_id": 1042, "urunler": [{"sku": "AM-101", "adet": 2}]} ← gövde (body)
Yanıt da benzer yapıdadır: bir durum kodu (status code), başlıklar ve gövde. REST tarzı API'lerde adres bir kaynağı (resource: siparişler, müşteriler), metot ise o kaynağa ne yapılacağını anlatır.
| Metot | Amaç | Gövde | Güvenli | Idempotent |
|---|---|---|---|---|
| GET | Kaynağı oku | Yok | Evet | Evet |
| POST | Yeni kaynak oluştur / işlem başlat | Var | Hayır | Hayır |
| PUT | Kaynağı tamamen değiştir | Var | Hayır | Evet |
| PATCH | Kaynağın bir kısmını güncelle | Var | Hayır | Genelde hayır |
| DELETE | Kaynağı sil | Genelde yok | Hayır | Evet |
Güvenli (safe) metot sunucuda bir şey değiştirmez. Idempotent metodu aynı istekle bir kez de on kez de çağırsan sonuç aynıdır. Bu ayrım yeniden denemede hayati önem taşır: GET'i tekrar denemek zararsızdır, ama zaman aşımına uğrayan bir ödeme POST'unu körlemesine tekrarlarsan müşteriden iki kez para çekebilirsin.
Durum kodları
| Kod | Anlamı | İstemci ne yapmalı? |
|---|---|---|
| 200 OK | Başarılı, gövdede veri var | JSON'u oku |
| 201 Created | Kaynak oluşturuldu | Yeni kaynağın kimliğini ya da Location başlığını al |
| 204 No Content | Başarılı, gövde yok | .json() çağırma |
| 301 / 302 / 304 | Taşındı / geçici yönlendirme / değişmedi | Kütüphane yönlendirmeyi izler; 304'te önbelleği (cache) kullan |
| 400 Bad Request | İstek hatalı (eksik alan, yanlış biçim) | Tekrar deneme; isteği düzelt |
| 401 Unauthorized | Kimlik doğrulanamadı (anahtar yok/yanlış/süresi dolmuş) | Token'ı yenile ya da yapılandırmayı düzelt |
| 403 Forbidden | Kim olduğun belli ama yetkin yok | Tekrar deneme; yetki iste |
| 404 Not Found | Kaynak yok | Çoğu zaman "yok" olarak işle (None) |
| 409 / 422 | Çakışma / doğrulama hatası | Gövdedeki hata ayrıntısını kullanıcıya göster |
| 429 Too Many Requests | Hız sınırı aşıldı | Retry-After kadar bekle, sonra tekrar dene |
| 500 / 502 / 503 / 504 | Sunucu ya da ağ geçidi sorunu | Geri çekilerek sınırlı sayıda tekrar dene |
requests ile istek atmak
requests Python'un en yaygın HTTP kütüphanesidir. Tarayıcı motorunda ağ olmadığı için bu kodu kendi bilgisayarında dene.
import os
import requests
TABAN = "https://api.anadolumarket.example/v1"
token = os.environ["ANADOLU_API_TOKEN"] # anahtar kodda değil, ortamda
with requests.Session() as oturum: # bağlantıyı yeniden kullanır, ortak başlık
oturum.headers.update({"Authorization": f"Bearer {token}", "Accept": "application/json"})
y = oturum.get(f"{TABAN}/siparisler", params={"sehir": "İzmir", "sayfa": 1}, timeout=10)
y.raise_for_status() # 4xx/5xx → requests.HTTPError
for s in y.json()["veri"]:
print(s["id"], s["tutar"])
yeni = oturum.post(f"{TABAN}/siparisler", json={"musteri_id": 1042, "tutar": 349.9}, timeout=(3, 10))
print(yeni.status_code, yeni.headers.get("Location"))
pip install requests httpx. Gerçek bir API ile denemek için herkese açık test servisleri (ör. httpbin) kullanabilirsin. httpx tarayıcı motorunda da yüklü; ağ isteği atamaz ama birazdan göreceğin sahte taşıyıcıyla tam çalışır.params=sorgu parametrelerini doğru kodlar (Türkçe karakter, boşluk,&). Adresi f-string ile elle kurma.json=gövdeyi JSON'a çevirir veContent-Typebaşlığını ekler;data=form gönderir.timeout=her zaman ver. Vermezsen istek sonsuza kadar bekleyebilir.(3, 10): 3 sn bağlantı, 10 sn okuma.SessionTCP bağlantısını yeniden kullanır; aynı sunucuya çok istek atıyorsan belirgin hızlanır.
httpx ve sahte taşıyıcıyla test
httpx, requests'e çok benzeyen modern bir kütüphanedir; ek olarak async desteği ve HTTP/2 sunar. En büyük test kolaylığı MockTransport'tur: istemci gerçek ağ yerine senin yazdığın bir fonksiyona istek gönderir. Böylece istemci kodun hiç değişmeden test edilir. Bu derste alıştırmaların çoğu bu yöntemi kullanıyor.
import httpx
def sahte_sunucu(istek: httpx.Request) -> httpx.Response:
if istek.headers.get("Authorization") != "Bearer gizli-123":
return httpx.Response(401, json={"hata": "yetkisiz"})
if istek.url.path == "/v1/siparisler/AM-7":
return httpx.Response(200, json={"id": "AM-7", "durum": "kargoda"})
return httpx.Response(404, json={"hata": "bulunamadı"})
istemci = httpx.Client(
transport=httpx.MockTransport(sahte_sunucu),
base_url="https://api.anadolumarket.example/v1",
headers={"Authorization": "Bearer gizli-123"},
timeout=5.0,
)
y = istemci.get("/siparisler/AM-7")
print(y.status_code, y.json())
y = istemci.get("/siparisler/AM-999")
print(y.status_code, y.is_success)
try:
y.raise_for_status()
except httpx.HTTPStatusError as e:
print("Hata:", e.response.status_code, e.request.url)
Sorgu parametreleri ve kimlik doğrulama
Sorgu parametreleri adresin ? sonrasındaki anahtar=değer çiftleridir. Özel karakterler yüzde kodlamasıyla (percent-encoding) yazılır; urlencode bunu senin yerine yapar.
from urllib.parse import urlencode
import base64
params = {"sehir": "İzmir", "q": "çay & şeker", "sayfa": 2, "kategori": None}
temiz = {k: v for k, v in params.items() if v is not None}
print(urlencode(temiz))
kullanici, parola = "rapor_bot", "s3cr3t"
kod = base64.b64encode(f"{kullanici}:{parola}".encode()).decode()
print("Authorization: Basic", kod)
| Yöntem | Nasıl gönderilir? | Not |
|---|---|---|
| API anahtarı | X-API-Key: abc123 başlığı (bazen ?api_key=) | Başlık tercih edilir; adres günlüklere düşer |
| Bearer token | Authorization: Bearer <token> | OAuth2/JWT; süresi dolar, yenilenir |
| Basic | Authorization: Basic base64(kullanıcı:parola) | Base64 şifreleme değildir; yalnız HTTPS ile |
| OAuth2 client credentials | Önce token uç noktasından token alınır | Kurumsal servisler arası yaygın yöntem |
Gizli anahtarlar: kodda değil, ortamda
API anahtarını koda yazıp Git'e göndermek en sık ve en pahalı hatalardan biridir: depo bir kez paylaşıldığında anahtar sızmış sayılır ve iptal edilmesi gerekir. Anahtarlar ortam değişkeninde (environment variable) ya da bir gizli anahtar yöneticisinde (secret manager) tutulur. Yerelde .env dosyası kullanıyorsan onu .gitignore'a ekle.
import os
def token_al(ortam=os.environ):
token = ortam.get("ANADOLU_API_TOKEN")
if not token:
raise RuntimeError("ANADOLU_API_TOKEN tanımlı değil; .env dosyanı ya da ortamını kontrol et")
return token
def maskele(deger):
return "****" + deger[-4:] if len(deger) > 4 else "****"
try:
token_al({})
except RuntimeError as e:
print("Hata:", e)
t = token_al({"ANADOLU_API_TOKEN": "tok_live_8f3a91c2"})
print("Kullanılan token:", maskele(t))
Authorization, X-API-Key, Cookie değerlerini maskele. Günlük sistemleri çoğu zaman uygulamadan çok daha fazla kişinin erişimine açıktır.Sayfalama
API'ler büyük listeleri parça parça verir. Üç yaygın biçim vardır ve istemcinin döngüsü buna göre kurulur:
| Biçim | İstek | Durma koşulu |
|---|---|---|
| Sayfa numarası | ?sayfa=3&boyut=100 | Boş liste ya da sayfa == toplam_sayfa |
| Offset/limit | ?offset=200&limit=100 | Dönen kayıt sayısı < limit |
| İmleç (cursor) | ?imlec=eyJpZCI6... | Yanıttaki sonraki_imlec boş |
| Link başlığı | Yanıtta Link: <...>; rel="next" | rel="next" yok |
İmleç tabanlı sayfalama, veri arada değişse bile kayıt atlamaz ya da tekrarlamaz; büyük ve canlı veri için en sağlam yöntemdir. Her durumda bir en fazla sayfa sınırı koymak, hatalı bir API'nin istemcini sonsuz döngüye sokmasını önler.
Hız sınırı ve üstel geri çekilme
Çoğu API dakikada belli sayıda isteğe izin verir; aşarsan 429 döner ve çoğu zaman Retry-After başlığıyla kaç saniye beklemen gerektiğini söyler. Geçici sunucu hatalarında (502, 503, 504) ise üstel geri çekilme (exponential backoff) uygulanır: 1 sn, 2 sn, 4 sn, 8 sn… Çok sayıda istemci aynı anda tekrar denemesin diye bekleme süresine küçük bir rastgelelik (jitter) eklenir.
import random
def bekleme(deneme, retry_after=None, taban=1.0, tavan=30.0, rng=random.Random(0)):
if retry_after is not None and str(retry_after).isdigit():
return float(retry_after)
ust = min(tavan, taban * 2 ** deneme)
return round(rng.uniform(0, ust), 2) # "full jitter"
for i in range(6):
print(i, bekleme(i))
print("Retry-After ile:", bekleme(0, retry_after="7"))
- Yalnızca geçici hataları tekrar dene: zaman aşımı, bağlantı hatası, 408, 429, 502, 503, 504.
- 400, 401, 403, 404, 422 gibi istemci hatalarını tekrar deneme; aynı istek aynı sonucu verir.
- Deneme sayısını sınırla (genelde 3–5) ve son denemeden sonra hatayı yukarı fırlat; sessizce
Nonedöndürme. - Idempotent olmayan istekleri (POST) yalnızca sunucu destekliyorsa bir
Idempotency-Keybaşlığıyla tekrar dene. - Her denemeyi günlüğe yaz: hangi adres, hangi kod, ne kadar beklendi.
Hata yönetimi ve yanıt doğrulama
| requests | httpx | Ne zaman? |
|---|---|---|
requests.Timeout | httpx.TimeoutException | Süre doldu |
requests.ConnectionError | httpx.ConnectError (TransportError) | DNS, bağlantı reddi, ağ kopması |
requests.HTTPError | httpx.HTTPStatusError | raise_for_status() 4xx/5xx gördü |
requests.RequestException | httpx.HTTPError | Hepsinin atası |
2xx almak verinin doğru olduğu anlamına gelmez. Dış sistemden gelen her yanıtı sınırda doğrula: beklenen alanlar var mı, tipleri doğru mu, değerler mantıklı mı? Küçük işlerde elle kontrol, büyük işlerde pydantic modelleri kullanılır. Doğrulanmamış veri sistemin içine sızarsa hata çok daha uzakta ve pahalı bir yerde ortaya çıkar.
Idempotency-Key ile gönderir (tekrar denemede çift randevu oluşmaz), tüm token'ları günlükte maskeler ve sağlayıcı tamamen çöktüğünde kullanıcıya "kaydınız sıraya alındı" deyip işi kuyruğa bırakır.İstemciyi sınıfa sar
Uygulamanın her yerinde httpx.get çağırmak yerine tek bir KargoAPI sınıfı yaz: taban adres, kimlik, zaman aşımı, yeniden deneme ve doğrulama bu sınıfta olsun; dışarıya gonderi_durumu(takip_no) gibi iş diliyle konuşan metotlar açsın. Testlerde sınıfa MockTransport'lu bir httpx.Client enjekte edersin.
Devre kesici (circuit breaker)
Karşı servis tamamen çöktüyse her isteği 4 kez denemek hem seni yavaşlatır hem onu daha çok yorar. Devre kesici, art arda belli sayıda hata görünce bir süre hiç istek atmadan hemen hata döner, sonra tek bir deneme isteğiyle servisin düzelip düzelmediğine bakar.
Önbellek ve koşullu istek
Sık değişmeyen veride (ürün kataloğu, il listesi) yanıtın ETag başlığını sakla, sonraki istekte If-None-Match gönder; değişmediyse sunucu gövdesiz 304 döner. Hem hız sınırını korursun hem bant genişliğini.
Async istemci
Yüzlerce bağımsız isteği sırayla atmak yavaştır. httpx.AsyncClient ve asyncio.gather ile eşzamanlı istek atabilirsin; ama eşzamanlılığı (concurrency) bir semafor ile sınırla ki hız sınırına çarpmayasın. Ayrıntıları ileri seviyedeki async dersinde göreceksin.