दस्तावेज़ पर जाएँ
Python

एसिंक

`AsyncOpenEmail`: `OpenEmail` का हर मेथड, await के साथ, asyncio या trio पर।

एसिंक क्लाइंट

AsyncOpenEmail में वे सभी मेथड हैं जो OpenEmail में हैं, उन्हीं आर्ग्युमेंट और उन्हीं return types के साथ, और हर एक coroutine है जिसे आप await करते हैं। यह उन्हीं keyword आर्ग्युमेंट से बनता है, जो कुछ आप छोड़ देते हैं उसके लिए वही एनवायरनमेंट वेरिएबल पढ़ता है, और वही त्रुटियाँ raise करता है।

async_client.py
import asyncio from openemail import AsyncOpenEmail  async def main() -> None:    async with AsyncOpenEmail() as client:        sent = await client.emails.send({            'from': 'Acme Billing <[email protected]>',            'to': '[email protected]',            'subject': 'Your September invoice',            'text': 'Your invoice is attached.',        })         email = await client.emails.get(sent['id'])        print(email['status'], email['sentAt'])  asyncio.run(main())

async with ब्लॉक ख़त्म होने पर कनेक्शन पूल बंद कर देता है, चाहे ब्लॉक सामान्य रूप से ख़त्म हो या किसी exception के साथ। जो क्लाइंट प्रोसेस जितने समय तक चलता है, उसे स्टार्टअप पर एक बार बनाया जाता है और शटडाउन पर await client.aclose() से बंद किया जाता है। पूरे प्रोग्राम के लिए एक क्लाइंट काफ़ी है: उसके event loop पर कितने भी coroutines उसे एक साथ इस्तेमाल कर सकते हैं।

तैयार openemail क्लाइंट और init() सिंक्रोनस हैं, और इनका कोई एसिंक जुड़वाँ नहीं है। अपना AsyncOpenEmail वहाँ बनाइए जहाँ प्रोग्राम शुरू होता है और उसे उस कोड को सौंपिए जिसे उसकी ज़रूरत है, या उसे अपने फ़्रेमवर्क की application state पर रखिए।

पैकेज की parity जाँच दोनों क्लाइंट की मेथड-दर-मेथड तुलना करती है और जब कोई मेथड OpenEmail और AsyncOpenEmail पर अलग आर्ग्युमेंट लेता है तो फेल हो जाती है, इसलिए हर मेथड पेज दोनों का वर्णन करता है।

async for के साथ पेजिंग

list और list_all को बाकी हर मेथड की तरह await किया जाता है। iterate को नहीं: यह तुरंत एक async iterator लौटाता है, और async for हर पेज तब fetch करता है जब loop उस तक पहुँचता है, इसलिए loop से बाहर निकलते ही अनुरोध रुक जाते हैं।

async_paging.py
import asyncio from openemail import AsyncOpenEmail  async def main() -> None:    async with AsyncOpenEmail() as client:        page = await client.emails.list(status='failed', limit=50)        print(len(page['items']), page['nextCursor'])         complaints = await client.suppressions.list_all(reason='complaint')        print(len(complaints))         async for thread in client.threads.iterate(folder='inbox'):            print(thread['id'])  asyncio.run(main())

एक साथ कई कॉल

एक क्लाइंट एक साथ कितने भी अनुरोध संभाल सकता है। asyncio.gather उन्हें साथ-साथ शुरू करता है, और एक semaphore चल रहे अनुरोधों की संख्या को आपकी चुनी हुई संख्या तक सीमित रखता है।

gather.py
import asyncio from openemail import AsyncOpenEmailfrom openemail.types import SentEmailResource  async def main() -> None:    recipients = ['[email protected]', '[email protected]', '[email protected]']    gate = asyncio.Semaphore(8)     async with AsyncOpenEmail() as client:         async def welcome(address: str) -> SentEmailResource:            async with gate:                return await client.emails.send({                    'from': 'Acme <[email protected]>',                    'to': address,                    'subject': 'Welcome to Acme',                    'text': 'Your workspace is ready.',                })         results = await asyncio.gather(            *(welcome(address) for address in recipients),            return_exceptions=True,        )         for address, result in zip(recipients, results):            if isinstance(result, BaseException):                print(address, 'failed:', result)            else:                print(address, result['status'])  asyncio.run(main())

API आज सामान्य reads और writes की दर पर कोई आम सीमा नहीं लगाता, इसलिए कोई चीज़ आपके लिए अचानक आई भीड़ को धीमा नहीं करती, और ऐसी सीमा बाद में जोड़ी जा सकती है। जिन चीज़ों की वह गिनती करता है, उन पर 429 जवाब देता है: वर्कस्पेस का मासिक भेजने का कोटा, उसकी दैनिक AI कार्रवाइयाँ और प्रति घंटे 500 फ़ाइल अपलोड, आदि। इनमें से कोई भी Retry-After नहीं लाता, इसलिए क्लाइंट retry करने के बजाय तुरंत is_rate_limited true वाली OpenEmailApiError raise करता है। return_exceptions=True से gather पहले इनकार पर raise करने के बजाय हर नतीजा लौटाता है, अस्वीकार हुई कॉल को उसके exception के रूप में, और loop हर एक को पढ़ता है।

किसी task को रद्द करने से उसका अनुरोध भी रद्द हो जाता है, और बीच रास्ते रद्द हुआ send शायद पहले ही API तक पहुँच चुका हो। जिस send को आप रद्द करके फिर दोहरा सकते हैं, उसे अपनी ख़ुद की idempotency_key= दें, ताकि दोहराव दूसरा संदेश भेजने के बजाय पहले वाले को replay करे।

asyncio और trio

क्लाइंट anyio के ज़रिए sleep और timeout करता है और httpx के ज़रिए भेजता है, और ये दोनों किसी भी event loop पर चलते हैं, इसलिए वही main() asyncio.run(main()) के तहत भी चलता है और trio.run(main) के तहत भी। trio पैकेज की dependency नहीं है, इसलिए जब आप उसे इस्तेमाल करें तो उसे ख़ुद इंस्टॉल करें।

asyncio के अपने gather और Semaphore, जैसे ऊपर के उदाहरण में, सिर्फ़ asyncio पर चलते हैं। जिस कोड को दोनों पर चलना है, उसके लिए anyio के create_task_group और Semaphore इस्तेमाल करें, जिस पर पैकेज पहले से निर्भर है।

एक एसिंक एक्सेस टोकन

किसी व्यक्ति का OAuth से जोड़ा गया ऐप API कुंजी के बजाय एक्सेस टोकन रखता है, और उसे access_token= के रूप में देता है: या तो ख़ुद टोकन, या ऐसा फ़ंक्शन जो उसे लौटाए। फ़ंक्शन हर अनुरोध से पहले चलता है, इसलिए समय-सीमा पास आने पर वह टोकन को नया कर सकता है और क्लाइंट को कभी दोबारा बनाना नहीं पड़ता। AsyncOpenEmail पर यह एक async फ़ंक्शन हो सकता है, और क्लाइंट उसके लौटाए मान को await करता है।

async_token.py
import asyncioimport timefrom dataclasses import dataclass from openemail import AsyncOpenEmail from acme.auth import refresh_access_token  @dataclassclass CachedToken:    value: str = ''    expires_at: float = 0.0  cached = CachedToken()  async def access_token() -> str:    if cached.expires_at - time.time() < 60:        cached.value, lifetime = await refresh_access_token()        cached.expires_at = time.time() + lifetime     return cached.value  async def main() -> None:    async with AsyncOpenEmail(access_token=access_token) as client:        me = await client.me.get()        print(me['object'])  asyncio.run(main())

फ़ंक्शन को हल्का रखें, क्योंकि हर अनुरोध उसका इंतज़ार करता है: cache किया हुआ टोकन लौटाएँ और उसे सिर्फ़ समय-सीमा के क़रीब नया करें, जैसा ऊपर है। OpenEmail किसी coroutine का इंतज़ार नहीं कर सकता, इसलिए उसे दिया गया async फ़ंक्शन पहले अनुरोध पर ValueError raise करता है।

डिस्पोज़ेबल इनबॉक्स

create_async_temp_mail() create_temp_mail() का एसिंक जुड़वाँ है। इसमें कोई API कुंजी नहीं होती: create और list_domains कोई क्रेडेंशियल नहीं भेजते, और बाकी हर मेथड create का लौटाया टोकन inbox_token= के रूप में लेता है। किसी एक इनबॉक्स से बँधे क्लाइंट के लिए inbox_token= सीधे create_async_temp_mail को ही पास करें।

async_temp_mail.py
import asyncio import httpxfrom openemail import create_async_temp_mail  async def main() -> None:    async with httpx.AsyncClient(follow_redirects=True) as http:        temp = create_async_temp_mail(http_client=http)        inbox = await temp.create({'ttlMinutes': 60})        print(inbox['address'], inbox['expiresAt'])         async for message in temp.iterate_messages(inbox['id'], inbox_token=inbox['token']):            print(message['from']['email'], message['subject'])  asyncio.run(main())

यह जो क्लाइंट लौटाता है उसका अपना कोई aclose() नहीं होता। काम पूरा होने पर उसके कनेक्शन बंद करने के लिए, async with से एक httpx.AsyncClient खोलें और उसे http_client= के रूप में पास करें, जैसा ऊपर है।

आपका अपना httpx क्लाइंट

http_client= आपका बनाया हुआ httpx.AsyncClient लेता है, proxy, कनेक्शन सीमाओं, अपने सर्टिफ़िकेट या टेस्ट में mock transport के लिए। httpx.Client देने पर TypeError raise होता है, क्योंकि वह OpenEmail के लिए है।

http_client.py
import asyncio import httpxfrom openemail import AsyncOpenEmail  async def main() -> None:    async with httpx.AsyncClient(        proxy='http://proxy.internal:3128',        limits=httpx.Limits(max_connections=20),        follow_redirects=True,    ) as http:        client = AsyncOpenEmail(http_client=http, timeout=20)        page = await client.threads.list(folder='inbox', limit=10)        print(len(page['items']))  asyncio.run(main())

आपका पास किया क्लाइंट आपका ही रहता है: aclose() और async with का अंत केवल वही पूल बंद करते हैं जिसे SDK ने खोला था, इसलिए अपना httpx.AsyncClient ख़ुद बंद करें, यहाँ उसके अपने async with से। SDK जो पूल खोलता है वह redirects का पालन करता है, इसलिए वैसा ही व्यवहार पाने के लिए अपने क्लाइंट पर follow_redirects=True सेट करें। क्लाइंट पर, या किसी एक कॉल पर, दिया गया timeout= फिर भी हर प्रयास की सीमा तय करता है, चाहे httpx.AsyncClient में कोई भी timeout हो।

टेस्ट में httpx.MockTransport हर अनुरोध का जवाब आपके एक फ़ंक्शन से देता है, इसलिए कुछ भी नेटवर्क तक नहीं पहुँचता।

test_with_mock.py
import asyncio import httpxfrom openemail import AsyncOpenEmail  def answer(request: httpx.Request) -> httpx.Response:    return httpx.Response(200, json={'object': 'list', 'data': [], 'hasMore': False, 'nextCursor': None})  async def main() -> None:    async with httpx.AsyncClient(transport=httpx.MockTransport(answer)) as http:        client = AsyncOpenEmail('oe_test_fixture', http_client=http)        page = await client.suppressions.list()        assert page['items'] == []  asyncio.run(main())