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

फ़्रेमवर्क

Django, Flask और FastAPI, एक webhook endpoint, और ऐसे background jobs जो कभी दो बार नहीं भेजते।

Django

कुंजी को settings में रखें, एनवायरनमेंट से पढ़कर, और एक अलग मॉड्यूल में एक OpenEmail बनाएँ जिसे views import करें। क्लाइंट को कई थ्रेड्स के बीच साझा करना सुरक्षित है, इसलिए एक ही instance हर अनुरोध को संभालता है और प्रोसेस के लिए एक ही कनेक्शन पूल रखता है। मॉड्यूल का नाम openemail.py के अलावा कुछ भी रखें, क्योंकि यह नाम पैकेज को छिपा सकता है।

settings.py
import os OPENEMAIL_API_KEY = os.environ['OPENEMAIL_API_KEY']OPENEMAIL_TIMEOUT = 10.0OPENEMAIL_SENDER = 'Acme <[email protected]>'
mailer.py
from django.conf import settingsfrom openemail import OpenEmail mailer = OpenEmail(settings.OPENEMAIL_API_KEY, timeout=settings.OPENEMAIL_TIMEOUT)
views.py
from django.conf import settingsfrom django.http import HttpRequest, JsonResponsefrom django.views.decorators.http import require_POSTfrom openemail import OpenEmailApiError from acme.mailer import mailer  @require_POSTdef invite(request: HttpRequest) -> JsonResponse:    try:        sent = mailer.emails.send({            'from': settings.OPENEMAIL_SENDER,            'to': request.POST['email'],            'subject': 'You are invited to Acme',            'text': 'Accept the invitation to join the workspace.',        })    except OpenEmailApiError as error:        if error.is_validation:            return JsonResponse({'error': error.message, 'field': error.param}, status=422)         raise     return JsonResponse({'id': sent['id']}, status=202)

पैकेज के साथ आने वाला openemail क्लाइंट भी यहाँ काम करता है: init(settings.OPENEMAIL_API_KEY) को एक बार, अपने app config के ready() मेथड से, कॉल करें, और जहाँ भी भेजना हो वहाँ openemail import करें।

Flask

app factory ऐप के साथ ही क्लाइंट बनाता है और उसे app.extensions में रखता है, और views उस तक current_app के ज़रिए पहुँचते हैं। जो factory क्लाइंट भी स्वीकार करता है, उसमें टेस्ट httpx.MockTransport पर बना क्लाइंट पास कर सकता है।

app.py
import os from flask import Flask, current_app, requestfrom openemail import OpenEmail  def create_app(mailer: OpenEmail | None = None) -> Flask:    app = Flask(__name__)    app.extensions['openemail'] = mailer or OpenEmail(os.environ['OPENEMAIL_API_KEY'], timeout=10)     @app.post('/invites')    def invite() -> tuple[dict[str, str], int]:        client: OpenEmail = current_app.extensions['openemail']        sent = client.emails.send({            'from': 'Acme <[email protected]>',            'to': request.form['email'],            'subject': 'You are invited to Acme',            'text': 'Accept the invitation to join the workspace.',        })         return {'id': sent['id']}, 202     return app

FastAPI

lifespan में एक AsyncOpenEmail बनाएँ, ताकि वह उसी event loop का हो जो अनुरोधों को संभालता है और सर्वर रुकने पर बंद हो जाए, और उसे एक dependency के ज़रिए routes को सौंपें।

main.py
from collections.abc import AsyncIteratorfrom contextlib import asynccontextmanagerfrom typing import Annotated from fastapi import Body, Depends, FastAPI, Requestfrom openemail import AsyncOpenEmail  @asynccontextmanagerasync def lifespan(app: FastAPI) -> AsyncIterator[None]:    async with AsyncOpenEmail(timeout=10) as mailer:        app.state.openemail = mailer        yield  app = FastAPI(lifespan=lifespan)  def get_openemail(request: Request) -> AsyncOpenEmail:    mailer: AsyncOpenEmail = request.app.state.openemail    return mailer  Mailer = Annotated[AsyncOpenEmail, Depends(get_openemail)]  @app.post('/invites', status_code=202)async def invite(email: Annotated[str, Body(embed=True)], mailer: Mailer) -> dict[str, str]:    sent = await mailer.emails.send({        'from': 'Acme <[email protected]>',        'to': email,        'subject': 'You are invited to Acme',        'text': 'Accept the invitation to join the workspace.',    })     return {'id': sent['id'], 'status': sent['status']}

टेस्ट app.dependency_overrides[get_openemail] के ज़रिए क्लाइंट बदल देता है, इसलिए कोई भी route API तक नहीं पहुँचता।

वेबहुक एंडपॉइंट

हर delivery पर कार्रवाई करने से पहले उसे कच्ची body और अनुरोध के headers के साथ सत्यापित करें: FastAPI में await request.body() और request.headers, Django में request.body और request.headers, और Flask में request.get_data() और request.headers। header की खोज अक्षरों के case को अनदेखा करती है, इसलिए हर फ़्रेमवर्क का अपना headers ऑब्जेक्ट जैसा है वैसा ही काम करता है।

webhooks.py
import os from fastapi import FastAPI, HTTPException, Request, Responsefrom openemail import WEBHOOK_EVENTS, WebhookVerificationError, verify_webhook_signature from acme.jobs import handle_bounce app = FastAPI()  @app.post('/webhooks/openemail', status_code=204)async def openemail_webhook(request: Request) -> Response:    try:        event = verify_webhook_signature(            payload=await request.body(),            headers=request.headers,            secret=os.environ['OPENEMAIL_WEBHOOK_SECRET'],        )    except WebhookVerificationError as error:        raise HTTPException(status_code=400, detail='bad signature') from error     if event['type'] == WEBHOOK_EVENTS.EMAIL_BOUNCED:        handle_bounce.delay(event['id'], event['data'])     return Response(status_code=204)
webhook_views.py
import os from django.http import HttpRequest, HttpResponsefrom django.views.decorators.csrf import csrf_exemptfrom django.views.decorators.http import require_POSTfrom openemail import WebhookVerificationError, verify_webhook_signature from acme.models import ReceivedEvent  @csrf_exempt@require_POSTdef openemail_webhook(request: HttpRequest) -> HttpResponse:    try:        event = verify_webhook_signature(            payload=request.body,            headers=request.headers,            secret=os.environ['OPENEMAIL_WEBHOOK_SECRET'],        )    except WebhookVerificationError:        return HttpResponse('bad signature', status=400)     ReceivedEvent.objects.get_or_create(        id=event['id'],        defaults={'type': event['type'], 'data': event['data']},    )     return HttpResponse(status=204)

Django बिना CSRF token वाले POST को अस्वीकार करता है, और delivery में कोई token नहीं होता, इसलिए view csrf_exempt है: हस्ताक्षर ही साबित करता है कि अनुरोध OpenEmail से आया। request.META के बजाय request.headers पढ़ें, क्योंकि उसकी keys का नाम बदलकर HTTP_X_OPENEMAIL_SIGNATURE जैसे रूप में कर दिया जाता है।

जल्दी से 2xx के साथ जवाब दें और काम बाद में करें। जिस delivery को कोई जवाब नहीं मिलता, या 408, 425, 429 या 5xx मिलता है, उसे फिर से आज़माया जाता है, लगभग साढ़े 27 घंटों में 8 बार तक, और replay किसी event को उसी id के साथ फिर भेजता है, इसलिए जिन ids को आप संभाल चुके हैं उन्हें सहेजें और दोहराव को छोड़ दें।

बैकग्राउंड जॉब

job queue किसी task के विफल होने पर उसे फिर चलाती है, और कोई task उसका ईमेल निकल जाने के बाद भी विफल हो सकता है: रिस्पॉन्स खो गया, या worker काम पूरा करने से पहले रुक गया। उस चीज़ से निकाली गई idempotency_key= पास करें जिसने send को ज़रूरी बनाया। फिर task का हर रन वही कुंजी लेकर चलता है, इसलिए दोहराव दूसरा संदेश भेजने के बजाय मूल संदेश को replay करता है।

tasks.py
from celery import Task, shared_taskfrom openemail import OpenEmail, OpenEmailApiError, OpenEmailNetworkError mailer = OpenEmail()  @shared_task(bind=True, acks_late=True, max_retries=5)def send_receipt(self: Task, order_id: str, email: str) -> str:    try:        sent = mailer.emails.send(            {                'from': 'Acme Billing <[email protected]>',                'to': email,                'subject': f'Receipt for order {order_id}',                'text': f'Thank you for order {order_id}.',            },            idempotency_key=f'receipt:{order_id}',        )    except OpenEmailNetworkError as error:        raise self.retry(exc=error, countdown=30)    except OpenEmailApiError as error:        if error.is_server_error:            raise self.retry(exc=error, countdown=30)         raise     return sent['id']

acks_late=True के साथ Celery किसी task को तभी acknowledge करता है जब वह चल चुका हो, इसलिए रुक गए worker की वजह से अधूरा रह गया task फिर से deliver हो सकता है, जो यहाँ सुरक्षित है क्योंकि कुंजी उस दूसरे रन को replay में बदल देती है। RQ का Retry विफल job को फिर चलाता है, और असर वही होता है।

jobs.py
from redis import Redisfrom rq import Queue, Retry from openemail import OpenEmail mailer = OpenEmail()  def send_receipt(order_id: str, email: str) -> str:    sent = mailer.emails.send(        {            'from': 'Acme Billing <[email protected]>',            'to': email,            'subject': f'Receipt for order {order_id}',            'text': f'Thank you for order {order_id}.',        },        idempotency_key=f'receipt:{order_id}',    )     return sent['id']  queue = Queue(connection=Redis())queue.enqueue(send_receipt, 'AC-4192', '[email protected]', retry=Retry(max=5, interval=[10, 60, 300]))

कुंजी उसी चीज़ से निकालें जिसने send को ज़रूरी बनाया, घड़ी से कभी नहीं। यह 1 से 255 अक्षरों, अंकों, underscores, बिंदुओं, colons या hyphens से बनती है, इसलिए इसे ईमेल पते के बजाय किसी id से बनाएँ। body भी सिर्फ़ task के आर्ग्युमेंट से ही बनाएँ: उसी कुंजी और अलग body वाला दोहराव replay होने के बजाय 422 idempotency_key_reuse के साथ अस्वीकार होता है।

क्लाइंट को मॉड्यूल स्तर पर बनाएँ। यह अपने पहले अनुरोध तक कोई कनेक्शन नहीं खोलता, इसलिए parent से fork हुआ हर worker प्रोसेस अपना ख़ुद का कनेक्शन खोलता है।