Ачааллж байна...
Түвшин 5-ын бүх жишээ сүлжээг загварчилсан: asyncio.sleep(1.0) бол "сервер нэг секундэд хариулна" гэсэн хуурамч дүр. Загварчлал сургахад тохиромжтой: тоонууд тогтвортой, интернэт хэрэггүй, алдаа гарахгүй. Гэвч жинхэнэ сүлжээ өөр. Хариуны хугацаа хүсэлт бүрт хэлбэлзэнэ. Сервер заримдаа алдаа буцаана, заримдаа огт хариулахгүй. Улаанбаатараас Америкийн сервер хүртэлх зай өөрөө хэдэн зуун миллисекунд нэмнэ. Энэ хичээл загварчлалаас бодит байдал руу шилжинэ.
HTTP бол вэбийн хэл: хөтөч хуудас нээх, апп серверээс өгөгдөл авах бүрт HTTP хүсэлт явна. Course 3-т та FastAPI-аар HTTP сервер бүтээсэн. Энэ хичээлд эсрэг тал, HTTP клиент: хүсэлт илгээж, хариу хүлээн авах тал. Python-ий хамгийн алдартай HTTP сан requests синхрон тул Хичээл 14.1-ийн дүрмийг зөрчиж, loop-ийг блоклоно. Бидэнд асинхрон сан хэрэгтэй.
Бид httpx2-ийг хэрэглэнэ. Түүний түүх сонирхолтой бөгөөд бодит програмчлалын нэг сургамж агуулдаг. Олон жил Python-ий асинхрон HTTP клиентийн гол сонголт httpx байсан. Гэвч түүний сүүлийн хувилбар 2024 оны арванхоёрдугаар сард гарснаас хойш шинэчлэл зогссон. Pydantic, Course 3-т FastAPI-тай хамт хэрэглэсэн өгөгдөл шалгах сангийн баг, httpx-ийг үргэлжлүүлэн арчлах үүрэг хүлээж, httpx2 нэрээр гаргасан. API нь яг ижил: интернэтэд олон заавар import httpx гэж бичсэн байх бөгөөд тэдгээрийн кодыг нэрийг нь сольж л хэрэглэж болно. Сургамж нь: хамааралтай сангийн арчлагдаж байгаа эсэх нь түүний боломжуудаас дутуугүй чухал. Аюулгүй байдлын засвар авахгүй сан цаг хугацааны явцад эрсдэл болно.
Энэ хичээлд бид жинхэнэ сервер рүү хүсэлт илгээж, хариуг уншиж, олон хүсэлтийг зэрэг явуулж хэмжиж, сүлжээний алдааг Бүлэг 11-ийн загвараар өөрийн exception шатлалд хөрвүүлнэ. Эцэст нь сүлжээгүйгээр HTTP кодыг шалгах хэрэгсэл MockTransport-той танилцана: дараагийн хичээлийн нэгдсэн төсөлд тестүүд үүн дээр тулгуурлана.
Олон улсын захидал харилцааг төсөөлөөд үзээрэй. Та захидал бичнэ: хаяг, сэдэв, агуулга. Шуудан түүнийг хүргэнэ. Хүлээн авагч хариу бичнэ: эхний мөрөнд товч төлөв, "хүлээн авлаа, бүх зүйл хэвийн" эсвэл "ийм хүн энд байхгүй", дараа нь агуулга. Хүсэлт бол захидал, хариу бол хариу захидал, эхний мөрийн товч төлөв бол HTTP-ийн status code: 200 "хэвийн", 404 "олдсонгүй", 500 "серверийн дотоод алдаа".
Шуудангийн гурван өөр асуудал бий бөгөөд тэдгээрийг ялгах нь чухал. Нэгдүгээрт, хаяг буруу эсвэл шуудангийн газар хаалттай: захидал огт хүрээгүй. Энэ бол холболтын алдаа. Хоёрдугаарт, захидал хүрсэн боловч хариу хэзээ ч ирсэнгүй: та хэр удаан хүлээх вэ? Энэ бол timeout. Гуравдугаарт, хариу ирсэн, гэвч "энэ хүсэлтийг биелүүлэх боломжгүй" гэсэн: шуудан ажилласан, асуудал агуулгад. Энэ бол алдаатай status code. Гурвуулаа "амжилтгүй", гэвч тус бүрд өөр арга хэмжээ: эхнийхэд хаягаа шалгах, хоёр дахьд дахин оролдох, гурав дахьд хүсэлтээ засах.
Мөн нэг практик зүйл: олон захидал илгээх бол шуудангийн газартай гэрээ байгуулсан нь захидал бүрт шинээр бүртгүүлэхээс хурдан. HTTP-д тэр гэрээ бол client объект: сервертэй холболтыг нэг удаа тогтоож, олон хүсэлтэд дахин хэрэглэнэ.
pip install httpx2Хичээл 6.4 ба 9.4-ийн адил өөрийн virtual environment дотор суулгахыг санаарай. Жишээнүүдэд httpbin.org-ийг хэрэглэнэ: HTTP-г туршихад зориулсан үнэгүй, нийтийн сервер. Түүний хаягууд хүсэлтийн тухай мэдээллийг буцааж, тодорхой status code эсвэл саатлыг санаатайгаар үүсгэдэг. Энэ бол нийтийн үйлчилгээ тул заримдаа удаан эсвэл түр ажиллахгүй байж болно. Тийм үед ижил хаягийн бүтэцтэй postman-echo.com өөр сонголт болно.
python
import asyncio
import httpx2
async def main():
async with httpx2.AsyncClient(base_url="https://httpbin.org", timeout=10.0) as client:
response = await client.get("/get", params={"хэл": "монгол"})
print(response.status_code)
print(response.headers["content-type"])
data = response.json()
print(data["args"])
asyncio.run(main())200
application/json
{'хэл': 'монгол'}AsyncClient бол шуудангийн гэрээ. Түүнийг Хичээл 14.2-ын async with-ээр нээж хаана: блок дуусахад сервертэй тогтоосон бүх холболт цэвэрхэн хаагдана. base_url нь бүх хүсэлтийн угтвар: дараа нь зөвхөн “/get” гэж бичихэд хангалттай. timeout нь секундээр: сервер арван секундэд хариулахгүй бол хүлээхээ болино. Анхдагч timeout таван секунд бөгөөд үүнийг тодорхой бичих нь кодыг уншигчид таны хүлээлтийг хэлнэ.
client.get() бол coroutine: await хийхэд хүсэлт явж, хариу ирэхийг хүлээх хооронд loop бусад ажилд ээлж өгнө. params нь URL-ийн асуултын хэсгийг үүсгэнэ: “?хэл=монгол”, кирилл үсгийг автоматаар кодолно. Хариуны объект гурван гол хэсэгтэй. status_code бол захидлын эхний мөр. headers бол dict шиг хариуны нэмэлт мэдээлэл: энд агуулга JSON гэдгийг хэлж байна. json() нь хариуны биеийг Python dict эсвэл list болгоно: Course 2-ын json.loads-ийг автоматаар хийдэг. httpbin бидний илгээсэн параметрийг буцаасан нь хүсэлт зөв явсныг батлав.
httpbin.org-ийн “/delay/1” хаяг хариулахаасаа өмнө нэг секунд санаатайгаар хүлээдэг: Хичээл 14.1-ийн fetch-ийн жинхэнэ хувилбар. Таван хүсэлт:
python
import time
async def get_ok(client, path):
response = await client.get(path)
response.raise_for_status()
return response.status_code
async def sequential(client, count):
for _ in range(count):
await get_ok(client, "/delay/1")
async def concurrent(client, count):
async with asyncio.TaskGroup() as group:
for _ in range(count):
group.create_task(get_ok(client, "/delay/1"))
async def main():
async with httpx2.AsyncClient(base_url="https://httpbin.org", timeout=15.0) as client:
for label, work in (("Дараалсан", sequential), ("Зэрэг", concurrent)):
start = time.perf_counter()
await work(client, 5)
print(f"{label:<10} {time.perf_counter() - start:.2f} с")
asyncio.run(main())Улаанбаатараас нэг ажиллуулалт:
Дараалсан 6.48 с
Зэрэг 1.41 сТаны тоонууд сүлжээ, цаг, серверийн ачааллаас хамаарч мэдэгдэхүйц өөр байх нь хэвийн. Харьцаа нь ойролцоо байх ёстой. Загварчлалаас ялгааг хараарай. Хүсэлт бүр нэг секунд биш, ойролцоогоор 1.3 секунд: нэмэлт 0.3 секунд нь Улаанбаатараас серверт хүрэх, буцах зам, мөн холболт тогтоох ажил. Зэрэг хувилбар 1.0 биш, 1.41 секунд: хамгийн удаан хүсэлтийн хугацаа. Бодит сүлжээнд хүсэлтүүд хэзээ ч яг ижил хугацаатай байдаггүй. Гэвч гол дүгнэлт хэвээр: таван хүлээлт давхцсан, бараг тав дахин хурдан.
Таван task нэг client-ийг хуваалцсан. Энэ зөв загвар: AsyncClient олон зэрэг хүсэлтэд зориулагдсан, тэр сервертэй хэд хэдэн холболтыг нээлттэй байлгаж, хүсэлтүүдэд дахин хэрэглэнэ. Хүсэлт бүрт шинэ client үүсгэвэл хүсэлт бүр холболтоо эхнээс нь тогтоох ёстой. HTTPS-д энэ нь шифрлэлтийн "гар барих" ажлыг багтаадаг бөгөөд холын серверт хэдэн зуун миллисекунд нэмнэ. Нэг програм, нэг client бол дүрэм.
raise_for_status() нь status code 400 ба түүнээс дээш бол HTTPStatusError шиднэ. Харин бусад алдааг httpx2 өөрийн шатлалаар шиддэг. Бидний сонирхох хэсэг:
httpx2.HTTPError бүх алдааны үндэс. Түүний доор хоёр салбар. RequestError: хариу ирээгүй. Түүний хүүхэд TimeoutException нь хугацаа хэтэрсэн, ConnectError нь холболт тогтоогоогүй. Хоёр дахь салбар HTTPStatusError: хариу ирсэн, гэвч алдаатай status code.
Энэ шатлал зүйрлэлийн гурван асуудалтай яг таарна. Гэвч програмын бусад хэсэгт httpx2-ийн алдааны нэрс тарах нь зөв биш: Хичээл 11.1-ийн зарчмаар програм өөрийн хэлээр ярих ёстой, гадны сан солигдвол програмын бусад хэсэг өөрчлөгдөх ёсгүй. httpx-ээс httpx2 руу шилжилт яг ийм солилтын бодит жишээ. Хичээл 11.2-ын raise from-оор хөрвүүлье:
python
class FetchError(Exception):
"""Сүлжээнээс өгөгдөл авах үеийн бүх алдааны үндэс."""
class FetchTimeoutError(FetchError):
"""Сервер заасан хугацаанд хариулсангүй. Дахин оролдож болно."""
class BadStatusError(FetchError):
"""Сервер алдаатай status code буцаалаа."""
def __init__(self, path, status):
super().__init__(f"{path}: сервер {status} буцаалаа")
self.path = path
self.status = status
async def fetch_json(client, path):
try:
response = await client.get(path)
response.raise_for_status()
except httpx2.TimeoutException as error:
raise FetchTimeoutError(f"{path}: хугацаа хэтэрлээ") from error
except httpx2.HTTPStatusError as error:
raise BadStatusError(path, error.response.status_code) from error
except httpx2.RequestError as error:
raise FetchError(f"{path}: холболтын алдаа") from error
else:
return response.json()except-үүдийн дараалал Хичээл 11.1-ийн дүрмээр: илүү нарийн TimeoutException нь өргөн RequestError-ээс өмнө. Эс бөгөөс RequestError timeout-ийг ч барьж, FetchTimeoutError хэзээ ч шидэгдэхгүй. else блок Хичээл 11.3-ын нарийн try зарчим: try дотор зөвхөн сүлжээний хэсэг. Хэрэв хариуны бие JSON биш бол response.json() өөрийн алдааг шиднэ, бид түүнийг сүлжээний алдаа гэж буруу ангилахгүй. BadStatusError status-ийг attribute-ээр хадгална: дуудагч 404 ба 503-ийг ялгаж, өөр шийдвэр гаргаж болно.
Гурван тохиолдлыг туршъя. httpbin-ийн “/status/503” хаяг санаатайгаар 503 буюу "үйлчилгээ түр боломжгүй" буцаана, “/delay/5” таван секунд хүлээнэ:
python
async def main():
async with httpx2.AsyncClient(base_url="https://httpbin.org", timeout=2.0) as client:
for path in ("/get", "/status/503", "/delay/5"):
try:
data = await fetch_json(client, path)
print(f"{path}: амжилттай, {len(data)} түлхүүр")
except BadStatusError as error:
print(f"{error} (status={error.status})")
except FetchTimeoutError as error:
print(f"{error}, шалтгаан: {type(error.__cause__).__name__}")
asyncio.run(main())/get: амжилттай, 4 түлхүүр
/status/503: сервер 503 буцаалаа (status=503)
/delay/5: хугацаа хэтэрлээ, шалтгаан: ReadTimeouttimeout-ийг 2 секунд болгосон тул таван секундийн хүлээлт таслагдсан. __cause__ анхны httpx2 алдааг хадгалсан: ReadTimeout, TimeoutException-ий хүүхэд, "хариуг уншиж байхад хугацаа хэтэрсэн". Хичээл 11.2-ын гинж: програм өөрийн хэлээр ярина, гэвч дебаг хийх үед анхны шалтгаан алга болоогүй.
Одоо Түвшин 5-ын хэрэгслүүдийг нэг функцэд нийлүүлье: Хичээл 14.3-ын Semaphore, 14.2-ын TaskGroup ба өөрийн алдааг барих, 15.1-ийн бэлэн болсон дарааллаар урсгах async generator:
python
async def fetch_all(client, paths, limit=3):
semaphore = asyncio.Semaphore(limit)
async def one(path):
async with semaphore:
try:
return path, await fetch_json(client, path), None
except FetchError as error:
return path, None, error
tasks = [asyncio.create_task(one(path)) for path in paths]
try:
for next_done in asyncio.as_completed(tasks):
yield await next_done
finally:
for task in tasks:
task.cancel()
async def main():
paths = ["/delay/2", "/get", "/status/404", "/delay/1", "/uuid"]
start = time.perf_counter()
async with httpx2.AsyncClient(base_url="https://httpbin.org", timeout=10.0) as client:
async for path, data, error in fetch_all(client, paths):
elapsed = time.perf_counter() - start
status = "алдаа: " + str(error) if error else "амжилттай"
print(f"{elapsed:5.2f} с {path:<12} {status}")
asyncio.run(main()) 0.34 с /get амжилттай
0.36 с /status/404 алдаа: /status/404: сервер 404 буцаалаа
0.68 с /uuid амжилттай
1.65 с /delay/1 амжилттай
2.33 с /delay/2 амжилттайДотоод функц one нь Бүлэг 4-ийн closure: semaphore ба client-ийг хадгалж байна. Тэр FetchError-ийг барьж, (зам, өгөгдөл, алдаа) гэсэн tuple буцаана: нэг замын алдаа бусдыг зогсоохгүй, Хичээл 12.4-ийн импортлогчийн "алдаатай мөрийг цуглуулж үргэлжлүүлэх" загвар. Хурдан хариунууд хамгийн түрүүнд, “/delay/2” хамгийн сүүлд ирсэн: бэлэн болох дарааллаар. Semaphore нэг удаад гурваас олон хүсэлт явуулаагүй тул “/uuid” эхний гуравын нэг нь дуусахыг хүлээсэн. Хичээл 15.1-ийн дүрмээр энэ generator-ийг эрт орхих газарт aclosing-оор боох хэрэгтэй.
Жинхэнэ сервертэй тест хоёр асуудалтай: интернэтгүй үед бүтэлгүйтнэ, сервер удаан эсвэл унтарсан үед санамсаргүй бүтэлгүйтнэ. Хичээл 12.4-т хурдны тест яг ийм шалтгаанаар найдваргүй гэсэн. httpx2 энэ асуудлыг MockTransport-оор шийддэг: client-ийн доор жинхэнэ сүлжээний оронд таны бичсэн функц хариулна:
python
def fake_shop(request):
if request.url.path == "/orders":
return httpx2.Response(200, json=[{"id": 1, "amount": 25_000}])
if request.url.path == "/slow":
raise httpx2.ReadTimeout("хуурамч timeout", request=request)
return httpx2.Response(404)
async def main():
transport = httpx2.MockTransport(fake_shop)
async with httpx2.AsyncClient(transport=transport, base_url="https://shop.test") as client:
print(await fetch_json(client, "/orders"))
for path in ("/missing", "/slow"):
try:
await fetch_json(client, path)
except FetchError as error:
print(type(error).__name__, error)
asyncio.run(main())[{'id': 1, 'amount': 25000}]
BadStatusError /missing: сервер 404 буцаалаа
FetchTimeoutError /slow: хугацаа хэтэрлээfake_shop нь хүсэлт аваад хариу буцаадаг энгийн функц: Course 3-т FastAPI-аар бичсэн endpoint-уудын жижиг хувилбар. “shop.test” хаяг огт байхгүй: ямар ч хүсэлт интернэт рүү гараагүй. Гэвч fetch_json жинхэнэ сервертэй харьцаж байгаа мэт ажилласан, гурван зам гурван тохиолдлыг үнэн зөвөөр үүсгэсэн. timeout-ийг хүртэл хүлээлгүйгээр, агшин зуурт загварчилсан. Энэ бол Хичээл 9.3-ын Factory ба Хичээл 10.3-ын Protocol-ийн санаа: client сүлжээг шууд мэддэггүй, транспортын гэрээгээр харьцдаг, тэр гэрээг хангасан ямар ч объект сүлжээний оронд орж чадна. Дараагийн хичээлийн тестүүд бүгд энэ аргаар бичигдэнэ.
Хүсвэл fetch_json-д Хичээл 5.3-ын retry санааг нэмж үзээрэй: FetchTimeoutError эсвэл 503 status-ийн үед л дахин оролдож, 404-ийн үед шууд бууж өгөх. Ялгааг MockTransport-оор шалгаж болно: дуудлага бүрийг тоолдог fake_shop эхний хоёр удаа 503 буцааж, гурав дахь удаа 200 өгдөг. Сонирхвол fetch_all-ийн limit-ийг 1, 3, 5 болгож httpbin-ийн таван “/delay/1” хүсэлт дээр хэмжиж, Хичээл 14.3-ын давалгааны хэв маяг бодит сүлжээнд хэрхэн харагдахыг харж болно. Мөн серверүүд “429 Too Many Requests” буцаахдаа ихэвчлэн “Retry-After” header-ээр хэдэн секундийн дараа дахин оролдохыг хэлдэг. httpbin-ийн “/status/429”-ийг ашиглан response.headers-ийг шалгах нь бодит API-тай ажиллах сайн бэлтгэл. Эдгээр бүгд санал бөгөөд дараагийн хичээлүүд үүнийг хийсэн гэж үзэхгүй.
AttributeError: ‘coroutine’ object has no attribute ‘status_code’. “coroutine объектод status_code attribute байхгүй” гэсэн утгатай. response = client.get(path) гэж await-ийг мартахад гарна: client.get хариу биш, coroutine буцаасан. Хичээл 14.1-ийн “never awaited” анхааруулга ч дагалдаж магадгүй. AsyncClient-ийн бүх хүсэлтийн method-ийн өмнө await.
RuntimeError: Cannot send a request, as the client has been closed. “Client хаагдсан тул хүсэлт илгээх боломжгүй” гэсэн утгатай. async with блокийн гадна, блок дууссаны дараа client-ийг хэрэглэхэд гарна. Ихэвчлэн task-ууд блокийн дотор үүсч, тэдний зарим нь блок дууссаны дараа ажилласаар байхад гардаг: Хичээл 14.2-ын өнчин ажлууд. TaskGroup-ийг client-ийн async with дотор байрлуул, тэгвэл бүх task client хаагдахаас өмнө дуусна.
httpx2.HTTPStatusError: Server error ‘503 SERVICE UNAVAILABLE’ for url ‘https://httpbin.org/status/503’. "Серверийн алдаа ‘503 үйлчилгээ боломжгүй'" гэсэн утгатай, араас нь status code-ийн тайлбарын холбоос гарна. raise_for_status() барьагдаагүй үед гарна. 4xx эхлэлтэй код “Client error”, "таны хүсэлтийн алдаа" гэж, 5xx “Server error”, "серверийн алдаа" гэж бичигдэнэ. Энэ ялгаа retry-ийн шийдвэрт чухал: 5xx-д дахин оролдох утгатай, 4xx-д ихэвчлэн утгагүй. raise_for_status дуудаагүй бол алдаа огт шидэгдэхгүй: 404 хуудасны HTML-ийг response.json() задлах гэж оролдоод огт өөр, төөрөгдүүлэх алдаа гарна.
ModuleNotFoundError: No module named ‘httpx2’. “httpx2 модуль олдсонгүй” гэсэн утгатай. Сан суугаагүй, эсвэл өөр virtual environment-д суусан. pip install-ийг програмаа ажиллуулж буй орчинд ажиллуул. Хэрэв таны төсөл аль хэдийн httpx хэрэглэдэг бол хоёулаа зэрэг суух боломжтой, гэхдээ нэг төсөлд нэгийг нь сонгох нь зөв.
Асинхрон програмд requests хэрэглэх. Мессежгүй. requests.get() синхрон бөгөөд хүсэлт бүрт бүх loop-ийг блоклоно: Хичээл 14.1-ийн time.sleep-ийн бодит хувилбар, програм зөв ажиллана, гэхдээ зэрэгцээ байдалгүйгээр. Async код дотор HTTP-д AsyncClient. Хэрэв хуучин код requests-тэй бөгөөд түүнийг солих боломжгүй бол Хичээл 14.3-ын to_thread.
Асинхрон HTTP клиентэд бид httpx2-ийг хэрэглэнэ: httpx-ийн арчлагдаж буй үргэлжлэл, API нь ижил. Сангийн арчлагдаж байгаа эсэх нь түүний боломжуудаас дутуугүй чухал. AsyncClient бол сервертэй холболтуудыг хадгалж дахин хэрэглэдэг гэрээ: async with-ээр нээж хаана, програм бүхэлдээ нэгийг хуваалцана. base_url бүх хүсэлтийн угтвар, timeout хариу хүлээх дээд хугацаа. await client.get() хариу буцаана: status_code, headers, json(). raise_for_status() алдаатай status-ийг exception болгоно. Бодит сүлжээнд хүсэлт бүрийн хугацаа хэлбэлзэж, холын зай ба холболт тогтоох ажил нэмэгдэнэ, гэвч зэрэг хүсэлтүүд хүлээлтийг давхцуулсаар: хэмжилтэд 6.48 секунд 1.41 болсон. httpx2-ийн алдаа гурван ангилалтай: timeout, холболтын алдаа, алдаатай status. Тэдгээрийг raise from-оор өөрийн exception шатлалд хөрвүүлж, програм өөрийн хэлээр ярихыг, анхны шалтгааныг хадгалахыг хослуулна. Semaphore, TaskGroup эсвэл create_task ба async generator нийлж олон хүсэлтийг хязгаартай, зэрэг, бэлэн болох дарааллаар урсгана. MockTransport нь client-ийн доорх сүлжээг таны функцээр сольж, HTTP кодыг интернэтгүй, хурдан, тогтвортой туршина.
Дараагийн хичээл бол энэ түвшний, мөн бүх сургалтын нэгдсэн төсөл. Бид олон эх үүсвэрээс өгөгдөл зэрэг цуглуулдаг асинхрон хэрэгсэл бүтээнэ. Тэнд энэ хичээлийн httpx2 ба MockTransport, Бүлэг 14-ын TaskGroup, timeout ба Semaphore, Хичээл 15.1-ийн async generator, Түвшин 4-ийн typing, exception шатлал ба хэмжилт, Түвшин 3-ын dataclass ба protocol, Түвшин 2-ын decorator ба context manager нэг програмд нэгдэнэ. Өмнөх build-үүдийн адил Git branch, pytest, commit-оор дуусна: энэ удаа эцсийнх.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.