অধ্যায় 3 · HTTP ও ওয়েব API
Python-এর requests দিয়ে API কল
- পৃষ্ঠা 14 / 22
- 14 মিনিট পড়া
Python for AI-তে আপনি প্রথম একটা requests.get কল করেছিলেন। এই পাতায় সেটাকে পাকা দক্ষতায় পরিণত করা হবে। এতদিন curl দিয়ে Practice API-র সাথে কথা বলেছেন; এবার সেটাই করবেন Python থেকে, যেখানে উত্তরগুলো ডিকশনারি, লিস্ট আর ভেরিয়েবল হয়ে আসে, যেগুলো আপনি লুপে ঘোরাতে, পরিষ্কার করতে, সংরক্ষণ করতে আর মডেলে পাঠাতে পারেন। প্রায় প্রতিটা AI প্রোডাক্টই কোথাও না কোথাও এটা করে: ডেটা আনে, কোনো মডেলের API ডাকে, কিংবা অন্য টিমের সার্ভিস ডাকে।
লক্ষ্য শুধু আজ কাজ করে এমন একটা কল লেখা নয়। লক্ষ্য হলো এমন কল লেখা যা নেটওয়ার্ক ধীর হলে, সার্ভার বন্ধ থাকলে, কি (key) না থাকলে, বা উত্তর আশানুরূপ না এলেও ঠিকমতো আচরণ করে। এই চারটা পরিস্থিতিতেই নতুনদের স্ক্রিপ্ট চিরকাল ঝুলে থাকে, ট্রেসব্যাকের দেয়াল নিয়ে ক্র্যাশ করে, কিংবা চুপচাপ ভুল ডেটা সেভ করে ফেলে।
যা শিখবেন
requestsইনস্টল করা, আর কুয়েরি প্যারামিটার, হেডার ও JSON বডি দিয়ে GET ও POST কল করা।- রেসপন্স পড়া: স্ট্যাটাস কোড, হেডার, টেক্সট ও JSON, আর
raise_for_status()দিয়ে সফল-ব্যর্থ ঠিক করা। requests.exceptions-এর এক্সেপশন দিয়ে ব্যর্থতা সামলানো, আর প্রতিটা কলে কেনtimeoutলাগে।- API কি এনভায়রনমেন্ট ভেরিয়েবলে রাখা, কোডে কখনোই নয়।
- পেজিনেটেড API (
next_page) ছোট একটা লুপ বা জেনারেটর দিয়ে পুরোটা পড়া।
সেট আপ
Practice API-কে তার নিজের দ্বিতীয় টার্মিনালে চালু রাখুন (fastapi run practice_api.py --port 8800, ওয়েব API ও REST: ডকুমেন্টেশন পড়া আর curl দিয়ে পরীক্ষা পাতা থেকে)। প্রথম টার্মিনালে ভার্চুয়াল এনভায়রনমেন্টসহ একটা প্রজেক্ট বানিয়ে requests ইনস্টল করুন:
mkdir -p ~/projects/api-client
cd ~/projects/api-client
python -m venv .venv
source .venv/bin/activate
pip install -q requestsuv দিয়ে একই কাজ হলো uv init, তারপর uv add requests। Windows PowerShell-এ অ্যাক্টিভেট করতে .venv\Scripts\Activate.ps1 ব্যবহার করুন। নিচের উদাহরণগুলো Python; একটা ফাইলে লিখে python file.py দিয়ে চালাতে পারেন, কিংবা Python REPL বা নোটবুকে লিখতে পারেন। নোটবুকের মতো এক ব্লকের ভেরিয়েবল পরের ব্লকে পাওয়া যাবে ধরে নেওয়া হয়েছে।
প্রথম GET
import requests
BASE = "http://localhost:8800"
response = requests.get(f"{BASE}/health", timeout=5)
print(response.status_code)
print(response.headers["content-type"])
print(response.text)
print(response.json())200
application/json
{"status":"ok"}
{'status': 'ok'}requests.get(url) একটা GET রিকোয়েস্ট পাঠায় আর একটা Response অবজেক্ট ফেরত দেয়। সার্ভার যা পাঠিয়েছে সবই এর ভেতরে থাকে। যে অ্যাট্রিবিউটগুলো আপনি সারাক্ষণ ব্যবহার করবেন:
| অ্যাট্রিবিউট | এটা কী |
|---|---|
response.status_code | সংখ্যাটা: 200, 404, 503 … |
response.ok | স্ট্যাটাস 400-এর কম হলে True |
response.headers | রেসপন্স হেডার; নাম খোঁজা বড়-ছোট হাতের অক্ষরে আটকায় না |
response.text | বডি, স্ট্রিং হিসেবে |
response.json() | বডিকে JSON হিসেবে পার্স করে ডিকশনারি ও লিস্ট (JSON না হলে এরর দেয়) |
response.content | বডি, কাঁচা বাইট হিসেবে (ছবি, ফাইলের জন্য) |
response.url | আসলে যে চূড়ান্ত URL-এ রিকোয়েস্ট গেছে, কুয়েরি স্ট্রিংসহ |
response.elapsed | পাঠানো থেকে হেডার পাওয়া পর্যন্ত সময় |
response.request | যে রিকোয়েস্ট পাঠানো হয়েছিল, ডিবাগের জন্য |
timeout=5 লক্ষ করুন। এটা নিয়ে একটু পরেই ভালো করে বলব; এখন শুধু মনে রাখুন, আপনার লেখা প্রতিটা কলের অংশ এটা।
কুয়েরি প্যারামিটার
f-string দিয়ে হাতে ?level=beginner&per_page=3 বানানো যায়, কিন্তু তখন বিশেষ অক্ষর URL-এনকোড করার দায় আপনার। তার বদলে params-এ একটা ডিকশনারি দিন, requests কুয়েরি স্ট্রিং বানিয়ে এনকোড করে দেবে:
response = requests.get(
f"{BASE}/v1/courses",
params={"level": "beginner", "per_page": 3},
timeout=5,
)
print(response.url)
data = response.json()
print(data["total"], data["page"], data["next_page"])
for course in data["items"]:
print(course["id"], course["title"])http://localhost:8800/v1/courses?level=beginner&per_page=3
5 1 2
1 How AI Works
2 Python for AI
3 Maths for AIresponse.url দেখে সহজেই বোঝা যায় আপনি আসলে কী চেয়েছিলেন। params-এ None মান থাকলে সেটা বাদ যায়, আর লিস্ট দিলে একই কি বারবার আসে (?tag=a&tag=b)। এবার স্পেসওয়ালা শহরের নাম দিয়ে চেষ্টা করুন আর response.url দেখুন:
response = requests.get(f"{BASE}/v1/weather", params={"city": "Cox's Bazar"}, timeout=5)
print(response.url)
print(response.status_code, response.json())http://localhost:8800/v1/weather?city=Cox%27s+Bazar
404 {'detail': 'No weather data for "Cox\'s Bazar"'}JSON পড়া
response.json() সাধারণ Python অবজেক্ট ফেরত দেয়: JSON অবজেক্ট হয় dict, অ্যারে হয় list, null হয় None। তাই ডিকশনারি আর কমপ্রিহেনশন নিয়ে যা শিখেছেন সবই এখানে খাটে:
response = requests.get(f"{BASE}/v1/courses/2", timeout=5)
course = response.json()
print(course)
print(course["title"], "-", course["hours"], "hours")
listing = requests.get(f"{BASE}/v1/courses", params={"per_page": 20}, timeout=5).json()["items"]
short = [c["title"] for c in listing if c["hours"] <= 6]
print(short){'id': 2, 'title': 'Python for AI', 'level': 'beginner', 'hours': 12, 'rating': 4.8}
Python for AI - 12 hours
['How AI Works', 'Prompt Engineering', 'LLM APIs']দুটো অভ্যাস আপনাকে অপ্রত্যাশিত ঝামেলা থেকে বাঁচাবে। প্রথমত, কোনো ফিল্ড না-ও থাকতে পারলে course.get("rating") (না থাকলে None) ব্যবহার করুন; আর ফিল্ডটা না থাকা মানে বাগ, যেটা আপনি দেখতে চান, সেখানে course["title"] লিখুন। দ্বিতীয়ত, যাচাই না করে কোনো রেসপন্সে .json() ডাকবেন না: প্রক্সির এরর পেজ HTML হয়, আর তার ওপর .json() এক্সেপশন ছুড়বে।
স্ট্যাটাস কোড ও raise_for_status
requests-এ 404 বা 500 কোনো এক্সেপশন নয়। নেটওয়ার্ক পর্যায়ে কলটা সফল হয়েছে; সার্ভার শুধু "না" বলেছে। তাই স্ট্যাটাস আপনাকেই দেখতে হবে:
response = requests.get(f"{BASE}/v1/courses/99", timeout=5)
print(response.status_code, response.ok)
print(response.json())404 False
{'detail': 'Course 99 not found'}সবচেয়ে সংক্ষিপ্ত নিরাপদ ধরন হলো response.raise_for_status()। 2xx আর 3xx-এ এটা কিছুই করে না, আর 4xx ও 5xx-এ requests.exceptions.HTTPError ছোড়ে:
try:
response.raise_for_status()
except requests.exceptions.HTTPError as error:
print("HTTPError:", error)
print("status:", error.response.status_code)HTTPError: 404 Client Error: Not Found for url: http://localhost:8800/v1/courses/99
status: 404এক্সেপশনের বার্তায় স্ট্যাটাস আর URL থাকে, আর error.response হলো পুরো রেসপন্স, তাই সার্ভারের ব্যাখ্যাও (error.response.json()["detail"]) পড়া যায়। কখন কোনটা:
| পরিস্থিতি | কী ব্যবহার করবেন |
|---|---|
| যেকোনো ব্যর্থতা বাগ, বা স্ক্রিপ্ট থামিয়ে দেওয়া উচিত | কলের ঠিক পরেই raise_for_status() |
| নির্দিষ্ট কোডের অর্থ আছে (404 = "নেই, বানাও") | raise_for_status()-এর আগে if response.status_code == 404: |
| শুধু সফল হলে আলাদা কোড চালাতে চান | if response.ok: |
ডেটা পাঠানো: JSON দিয়ে POST
একটা রিভিউ বানাতে আপনি /v1/reviews-এ JSON POST করবেন, যার জন্য X-API-Key হেডারে একটা API কি-ও লাগে। json=-এ একটা ডিকশনারি দিন: requests সেটাকে JSON টেক্সটে বদলে নিজেই Content-Type: application/json বসিয়ে দেয়। হেডারও ডিকশনারি হিসেবে দিন:
review = {"course_id": 2, "rating": 5, "comment": "Clear and practical"}
headers = {"X-API-Key": "practice-key-123"}
response = requests.post(f"{BASE}/v1/reviews", json=review, headers=headers, timeout=5)
print(response.status_code)
print(response.json())201
{'id': 1, 'course_id': 2, 'rating': 5, 'comment': 'Clear and practical'}এখানে হেডারটা দেখানোর জন্য কি কোডে লেখা হয়েছে; দুটো সেকশন পরে সঠিক পদ্ধতি দেখবেন। তার আগে, json= আসলে কী পাঠায়? /v1/echo দেখিয়ে দেয়, সাথে data=-এর পার্থক্যও, যা একটা ফর্ম বডি পাঠায়:
as_json = requests.post(f"{BASE}/v1/echo", json={"name": "Nadia"}, timeout=5).json()
print(as_json["headers"]["content-type"], as_json["json"])
as_form = requests.post(f"{BASE}/v1/echo", data={"name": "Nadia"}, timeout=5).json()
print(as_form["headers"]["content-type"], as_form["json"])application/json {'name': 'Nadia'}
application/x-www-form-urlencoded name=Nadiaআধুনিক JSON API json= আশা করে। ডিকশনারি সহ data= ব্যবহার করা একটা চিরাচরিত ভুল: সার্ভার JSON নয়, একটা ফর্ম পায়, আর 422 বা 400 ফেরত দেয়। একই কল কি ছাড়া আর ভুল ডেটা দিয়ে করলে ব্যর্থতা কেমন দেখায়:
no_key = requests.post(f"{BASE}/v1/reviews", json=review, timeout=5)
print(no_key.status_code, no_key.json())
bad = requests.post(f"{BASE}/v1/reviews", json={**review, "rating": 9}, headers=headers, timeout=5)
print(bad.status_code)
problem = bad.json()["detail"][0]
print(problem["loc"], problem["msg"])401 {'detail': 'Missing API key: send it in the X-API-Key header'}
422
['body', 'rating'] Input should be less than or equal to 5401 মানে "আপনি কে?" (কি নেই), আর 422 মানে "JSON বুঝেছি, কিন্তু মানগুলো ঠিক নয়", আর সমস্যাওয়ালা ফিল্ডের নাম detail-এ থাকে। এই বার্তাগুলো পড়েই আপনি রিকোয়েস্ট ঠিক করবেন। অন্য মেথডগুলোও একইভাবে চলে: requests.put, requests.patch, requests.delete, আর মেথড ভেরিয়েবল হলে requests.request("GET", …)।
কী ভুল হতে পারে: এক্সেপশন
HTTP স্ট্যাটাস কল ব্যর্থ হওয়ার একটা উপায় মাত্র। কখনো কোনো রেসপন্সই আসে না: সার্ভার বন্ধ, নেটওয়ার্ক নেই, নাম রিজলভ হচ্ছে না, বা সার্ভার অনেক দেরি করছে। এসব ক্ষেত্রে requests একটা এক্সেপশন ছোড়ে। সবগুলো requests.exceptions.RequestException থেকে উত্তরাধিকার পায়:
| এক্সেপশন | অর্থ | সাধারণ কারণ |
|---|---|---|
ConnectionError | সংযোগ করা যায়নি | সার্ভার চলছে না, ভুল পোর্ট, DNS ব্যর্থ, ইন্টারনেট নেই |
Timeout (ConnectTimeout, ReadTimeout) | সময়মতো সংযোগ বা উত্তর পাওয়া যায়নি | ধীর বা অতিরিক্ত চাপে থাকা সার্ভার |
HTTPError | 4xx বা 5xx, raise_for_status() ছোড়ে | খারাপ রিকোয়েস্ট, কি নেই, সার্ভারের ত্রুটি |
JSONDecodeError | বডি বৈধ JSON নয় | HTML এরর পেজ, খালি বডি |
InvalidURL, MissingSchema | URL নিজেই ভুল | http:// লিখতে ভুলে যাওয়া |
চলুন ইচ্ছে করে দুটো ঘটাই। 8001 পোর্টে কিছু শুনছে না, আর /v1/slow আপনি যতক্ষণ বলবেন ততক্ষণ অপেক্ষা করে:
try:
requests.get("http://localhost:8001/health", timeout=2)
except requests.exceptions.ConnectionError as error:
print("ConnectionError: could not connect")
try:
requests.get(f"{BASE}/v1/slow", params={"seconds": 3}, timeout=1)
except requests.exceptions.Timeout as error:
print("Timeout:", type(error).__name__)ConnectionError: could not connect
Timeout: ReadTimeoutসবসময় টাইমআউট দিন
এই পাতার সবচেয়ে গুরুত্বপূর্ণ নিয়ম এটা। requests-এর কোনো ডিফল্ট টাইমআউট নেই: টাইমআউট না দিলে, যে সার্ভার সংযোগ নিয়ে তারপর চুপ করে যায় তার কলে চিরকাল অপেক্ষা করতে পারে, আর আপনার স্ক্রিপ্ট, নোটবুক বা পুরো ওয়েব সার্ভারের একটা ওয়ার্কার সেই সাথে আটকে যায়। প্রতিটা কলে সেকেন্ডে একটা timeout দিন:
timeout=5মানে সংযোগ করতে ৫ সেকেন্ড আর বাইটের মাঝে ডেটার জন্য অপেক্ষায় ৫ সেকেন্ড।timeout=(3.05, 30)সংযোগ আর রিড আলাদাভাবে ঠিক করে। সংযোগ দ্রুত হওয়ার কথা; একটা LLM-এর উত্তর সত্যিই বেশি সময় নিতে পারে, তাই রিডের জন্য বেশি দিন।
রিড টাইমআউট হলো সার্ভার যতক্ষণ চুপ থাকে সেই সময়, ডাউনলোডের মোট সময় নয়। স্বাভাবিক সময় দেখে মান ঠিক করুন: সাধারণ JSON API-র জন্য কয়েক সেকেন্ড, স্ট্রিমিং ছাড়া LLM কলের জন্য ৩০–১২০ সেকেন্ড। প্রতিবার না লিখতে একবারই সংজ্ঞায়িত করে নিন:
TIMEOUT = (3.05, 10)
started = requests.get(f"{BASE}/v1/slow", params={"seconds": 0.2}, timeout=TIMEOUT)
print(started.json(), started.elapsed.total_seconds() >= 0.2){'waited_seconds': 0.2} True(আমরা শুধু ছাপছি অপেক্ষা অন্তত 0.2 সেকেন্ড ছিল কি না, যাতে আউটপুট সব কম্পিউটারে একই হয়।)
সব একসাথে: একটা নিরাপদ fetch ফাংশন
এবার অংশগুলো মিলিয়ে একটা ফাংশন বানাই যা কখনো ঝুলে থাকে না, প্রতিটা ব্যর্থতাকে একটা পরিষ্কার বার্তায় বদলায়, আর সফল হলে ডেটা ফেরত দেয়। except ক্লজের ক্রম লক্ষ করুন: আগে HTTPError আর Timeout, সবশেষে সাধারণ RequestException:
def fetch_json(url, **kwargs):
"""GET a URL and return its JSON, or raise RuntimeError with a readable message."""
try:
response = requests.get(url, timeout=TIMEOUT, **kwargs)
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as error:
status = error.response.status_code
raise RuntimeError(f"server said {status} for {url}") from error
except requests.exceptions.Timeout as error:
raise RuntimeError(f"timed out calling {url}") from error
except requests.exceptions.ConnectionError as error:
raise RuntimeError(f"could not connect to {url}") from error
except requests.exceptions.JSONDecodeError as error:
raise RuntimeError(f"{url} did not return JSON") from error
except requests.exceptions.RequestException as error:
raise RuntimeError(f"request failed: {type(error).__name__}") from error
print(fetch_json(f"{BASE}/v1/weather", params={"city": "Sylhet"}))
for bad_url in (f"{BASE}/v1/courses/99", "http://localhost:8001/health"):
try:
fetch_json(bad_url)
except RuntimeError as error:
print("failed:", error){'city': 'Sylhet', 'temp_c': 27.4, 'humidity': 85, 'condition': 'Rain'}
failed: server said 404 for http://localhost:8800/v1/courses/99
failed: could not connect to http://localhost:8001/healthদুটো খুঁটিনাটি। raise … from error মূল এক্সেপশনটাকে জুড়ে রাখে, তাই ট্রেসব্যাকে দুটোই দেখা যায়। আর JSONDecodeError সাধারণ RequestException-এর আগে আসতে হবে, কারণ সেটা এর সাবক্লাস (বর্তমান requests-এ JSON নয় এমন বডিতে requests.exceptions.JSONDecodeError ওঠে); সাধারণ ক্লজ আগে থাকলে সেটাই ওটাকে গিলে ফেলত। পুরো কিছুকে খালি except Exception: pass-এ মুড়ে দেবেন না: আপনার দরকারি তথ্যটাই তা লুকিয়ে ফেলে।
এনভায়রনমেন্ট ভেরিয়েবল থেকে API কি
ওপরের practice-key-123 কোডের ভেতরেই ছিল। একটা নকল API-র জন্য তাতে ক্ষতি নেই; কিন্তু আসল কি-র (OpenAI, Anthropic, কোনো পেইড আবহাওয়া API) জন্য এটা ফাঁস হওয়ার অপেক্ষা, কারণ কোড যায় Git ও GitHub-এ (দেখুন .gitignore: সিক্রেট আর অপ্রয়োজনীয় ফাইল বাইরে রাখা)। নিয়ম হলো: গোপন তথ্য থাকে এনভায়রনমেন্টে, কোড সেটা পড়ে। Python চালানোর আগে টার্মিনালে:
export PRACTICE_API_KEY="practice-key-123"(PowerShell: $env:PRACTICE_API_KEY = "practice-key-123"। এনভায়রনমেন্ট ভেরিয়েবল, .env ফাইল ও ভার্চুয়াল এনভায়রনমেন্ট পাতার python-dotenv দিয়ে লোড করা .env ফাইলও একই কাজ করে, আর ডেভেলপমেন্টের সময় সেটাই সাধারণ পদ্ধতি।) তারপর Python-এ:
import os
def get_api_key():
key = os.environ.get("PRACTICE_API_KEY")
if not key:
raise RuntimeError("PRACTICE_API_KEY is not set. Export it or add it to .env")
return key
api_key = get_api_key()
print("key loaded:", len(api_key), "characters")
print("shown safely:", api_key[:4] + "..." + api_key[-3:])key loaded: 16 characters
shown safely: prac...123এক জায়গায় পড়া, আর না থাকলে পরিষ্কার এরর দেওয়া, পরে আসা রহস্যময় 401-এর চেয়ে ভালো। লগ করতেই হলে ওপরের মতো শুধু প্রথম আর শেষের কয়েকটা অক্ষর দেখান; পুরো কি কখনো ছাপবেন না, URL-এর কুয়েরি স্ট্রিংয়ে দেবেন না (সার্ভার ও প্রক্সি URL লগ করে), আর কমিট করবেন না। এখন রিভিউ কল পরিচ্ছন্ন:
headers = {"X-API-Key": get_api_key()}
response = requests.post(f"{BASE}/v1/reviews", json=review, headers=headers, timeout=TIMEOUT)
print(response.status_code, response.json()["id"])201 2বেয়ারার টোকেনও একইভাবে কাজ করে, শুধু হেডার আলাদা: {"Authorization": f"Bearer {token}"}। পরের পাতায় চ্যাট API-র জন্য এটা ব্যবহার করবেন।
পেজিনেটেড রেসপন্স পড়া
আসল API সাধারণত সবকিছু একবারে দেয় না। /v1/courses per_page সংখ্যক আইটেম (ডিফল্ট ৫) আর একটা next_page নম্বর দেয়, যা শেষ পেজে None হয়ে যায়। শুধু প্রথম রেসপন্স পড়লে আপনি চুপচাপ ডেটা হারান, ট্রেনিং বা ইভ্যালুয়েশন ডেটা জোগাড়ের সময় এটা নীরব কিন্তু খুব পরিচিত বাগ। সমাধান হলো next_page অনুসরণ করে চলা একটা লুপ:
def all_courses(**filters):
"""Yield every course, following next_page until it is None."""
page = 1
while page is not None:
data = fetch_json(f"{BASE}/v1/courses", params={**filters, "page": page, "per_page": 3})
yield from data["items"]
page = data["next_page"]
titles = [course["title"] for course in all_courses()]
print(len(titles))
print(titles)
print([c["title"] for c in all_courses(level="advanced")])10
['How AI Works', 'Python for AI', 'Maths for AI', 'SQL and Data Handling', 'Git, APIs and Backend', 'Prompt Engineering', 'LLM APIs', 'RAG Systems', 'AI Agents', 'LLMOps']
['RAG Systems', 'AI Agents', 'LLMOps']জেনারেটর (yield) আইটেমগুলো একটা একটা করে দেয়, তাই কলার আগেভাগে থামতে পারে, বা অনেক বড় কালেকশন সবটা মেমরিতে না রেখেই প্রসেস করতে পারে। সব একসাথে চাইলে list(...)-এ মুড়ে নিন। অন্য API অন্য আকার ব্যবহার করে: যেমন সরাসরি অনুসরণ করার মতো একটা next URL, বা ফেরত পাঠানোর মতো একটা অস্বচ্ছ cursor বা next_page_token। লুপ একই থাকে; শুধু ফিল্ডের নাম বদলায়, আর কোনটা তা API-র ডকুমেন্টেশনে লেখা থাকে। যে API আপনার নিয়ন্ত্রণে নেই, সেখানে একটা নিরাপত্তা সীমা (যেমন সর্বোচ্চ পেজ সংখ্যা) রাখুন, যাতে ওদের দিকের কোনো বাগ আপনাকে অনন্ত লুপে আটকাতে না পারে।
বাস্তব ব্যবহার
- ডেটা সংগ্রহ: ডকুমেন্ট, টিকেট বা পণ্যের তালিকা পেজ ধরে ধরে এনে ডেটাসেট বা RAG-এর সার্চ ইনডেক্সে ঢোকানো।
- মডেল প্রোভাইডার ডাকা: একটা LLM কল হলো JSON বডিসহ একটা POST, এনভায়রনমেন্ট থেকে নেওয়া বেয়ারার কি, আর উদার একটা টাইমআউট। এই পাতার কোডই ঠিক সেটা।
- সার্ভিসের মধ্যে আঠা: আপনার FastAPI ব্যাকএন্ড (পরের পাতাগুলোতে) অন্য সার্ভিসকে একই ফাংশন দিয়ে ডাকে, তাই সেখানে টাইমআউট বা কি না থাকলে একটা পরিষ্কার এরর আসা উচিত, রিকোয়েস্ট ঝুলে থাকা নয়।
সাধারণ ভুল
- টাইমআউট না দেওয়া। কল চিরকাল ঝুলে থেকে স্ক্রিপ্ট বা সার্ভার ওয়ার্কার জমিয়ে দিতে পারে। সবসময়
timeout=দিন। - 200 ধরে নেওয়া। এরর বডিতে
.json()ডেকে ইনডেক্স করলে বিভ্রান্তিকরKeyErrorআসে। আগে স্ট্যাটাস দেখুন বাraise_for_status()ডাকুন। json=-এর জায়গায়data=। সার্ভার JSON নয়, ফর্ম পায়, আর 422 বা 400 দেয়। JSON API-তেjson=ব্যবহার করুন।- কোডে বা URL-এ গোপন তথ্য। Git-এ কমিট হওয়া কি মানে ফাঁস হওয়া কি। এনভায়রনমেন্ট থেকে পড়ুন, হেডারে পাঠান।
- প্রথম পেজে থেমে যাওয়া। একটা লিস্ট এন্ডপয়েন্টের অনেক পেজ থাকতে পারে। শেষ না হওয়া পর্যন্ত
next_page(বা API-র সমতুল্য) অনুসরণ করুন। - সব এক্সেপশন ধরে ফেলা।
except Exception: passআসল বাগ লুকায়। যেগুলো সামলাতে পারেন সেই নির্দিষ্টrequests.exceptionsধরুন, বাকিগুলো ভেসে উঠতে দিন।
নিজে চেষ্টা করুন
- সহজ: ঢাকার আবহাওয়া এনে একটা লাইন ছাপুন:
Dhaka: 31.5 C, Humid। - সহজ: সবচেয়ে বেশি রেটিংয়ের তিনটা কোর্স আনুন: সব কোর্স (
per_page=20) চেয়ে রেটিং অনুযায়ী সাজিয়ে সেরাটা আগে রেখে শিরোনাম ছাপুন। - মাঝারি:
course_or_none(course_id)লিখুন, যা কোর্সের ডিকশনারি ফেরত দেয়, 404 হলেNoneদেয়, কিন্তু অন্য যেকোনো এররে এক্সেপশন ছোড়ে। ৩ আর ৯৯ আইডি দিয়ে পরীক্ষা করুন। - মাঝারি:
post_review(course_id, rating, comment)লিখুন, যা এনভায়রনমেন্ট থেকে কি নেয় আর নতুন রিভিউয়ের আইডি ফেরত দেয়; API 422 দিলে এটাNoneফেরত দেবে এবং প্রথম সমস্যার বার্তা ছাপবে। - কঠিন: আপনার
all_coursesজেনারেটর দিয়ে প্রতিটা লেভেলের মোট ঘণ্টা হিসাব করুন আর লেভেলগুলো বর্ণানুক্রমে ছাপুন।
উত্তর
# 1. আবহাওয়ার লাইন
w = fetch_json(f"{BASE}/v1/weather", params={"city": "Dhaka"})
print(f"{w['city']}: {w['temp_c']} C, {w['condition']}")
# 2. রেটিং অনুযায়ী সেরা তিন
courses = fetch_json(f"{BASE}/v1/courses", params={"per_page": 20})["items"]
for c in sorted(courses, key=lambda c: c["rating"], reverse=True)[:3]:
print(c["rating"], c["title"])Dhaka: 31.5 C, Humid
4.9 RAG Systems
4.8 Python for AI
4.8 AI Agents# 3. শুধু 404-এ None
def course_or_none(course_id):
response = requests.get(f"{BASE}/v1/courses/{course_id}", timeout=TIMEOUT)
if response.status_code == 404:
return None
response.raise_for_status()
return response.json()
print(course_or_none(3)["title"])
print(course_or_none(99))
# 4. post_review
def post_review(course_id, rating, comment):
response = requests.post(
f"{BASE}/v1/reviews",
json={"course_id": course_id, "rating": rating, "comment": comment},
headers={"X-API-Key": get_api_key()},
timeout=TIMEOUT,
)
if response.status_code == 422:
print("invalid:", response.json()["detail"][0]["msg"])
return None
response.raise_for_status()
return response.json()["id"]
print(post_review(1, 4, "Good start"))
print(post_review(1, 0, "Bad rating"))Maths for AI
None
3
invalid: Input should be greater than or equal to 1
None# 5. লেভেল অনুযায়ী ঘণ্টা
totals = {}
for course in all_courses():
totals[course["level"]] = totals.get(course["level"], 0) + course["hours"]
for level in sorted(totals):
print(level, totals[level])advanced 30
beginner 42
intermediate 11
সারসংক্ষেপ
requests.get/post(url, params=…, json=…, headers=…, timeout=…)একটাResponseফেরত দেয়, যাতেstatus_code,headers,textআর.json()থাকে।- 4xx বা 5xx নিজে থেকে এক্সেপশন নয়;
raise_for_status()ডাকুন বাstatus_codeদেখুন। - নেটওয়ার্কের ব্যর্থতায়
requests.exceptionsক্লাস ওঠে:ConnectionError,Timeout,HTTPError, সবইRequestException-এর অধীনে। - সবসময় টাইমআউট দিন;
requests-এর ডিফল্টে কোনোটা নেই। - কি রাখুন এনভায়রনমেন্ট ভেরিয়েবলে, এক জায়গায় পড়ুন, কখনো ছাপবেন না বা কমিট করবেন না।
- ডেটা না হারাতে লুপে
next_pageঅনুসরণ করুন (জেনারেটর বেশ পরিচ্ছন্ন)।
এরপর: নির্ভরযোগ্য API কল: টাইমআউট, রিট্রাই, পেজিনেশন ও LLM API পাতায় রিট্রাই ও ব্যাকঅফ, সেশন ও সেভ করা ফলাফল দিয়ে এই কলগুলোকে টেকসই করা হবে, তারপর আসল LLM-এর মতো করে চ্যাট API ডাকা হবে।