Framework-et
Django, Flask dhe FastAPI, një endpoint webhook-u, dhe punë në sfond që nuk dërgojnë kurrë dy herë.
Django
Mbajeni çelësin në settings, të lexuar nga mjedisi, dhe ndërtoni një OpenEmail në një modul më vete që e importojnë view-t. Klienti mund të ndahet pa rrezik mes thread-eve, ndaj një instancë i shërben çdo kërkese dhe mban një pool lidhjesh për procesin. Emërtojeni modulin si të doni, përveç openemail.py, që mund ta fshehë paketën.
import os OPENEMAIL_API_KEY = os.environ['OPENEMAIL_API_KEY']OPENEMAIL_TIMEOUT = 10.0OPENEMAIL_SENDER = 'Acme <[email protected]>'from django.conf import settingsfrom openemail import OpenEmail mailer = OpenEmail(settings.OPENEMAIL_API_KEY, timeout=settings.OPENEMAIL_TIMEOUT)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)Klienti i gatshëm openemail funksionon edhe këtu: thirreni init(settings.OPENEMAIL_API_KEY) një herë, nga metoda ready() e konfigurimit të aplikacionit tuaj, dhe importoni openemail kudo që dërgoni.
Flask
Factory e aplikacionit e ndërton klientin bashkë me aplikacionin dhe e mban në app.extensions, dhe view-t e arrijnë përmes current_app. Një factory që pranon edhe një klient i lejon një testi të japë një klient të ndërtuar mbi httpx.MockTransport.
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 appFastAPI
Ndërtoni një AsyncOpenEmail në lifespan, që t'i përkasë ciklit të ngjarjeve që u shërben kërkesave dhe të mbyllet kur ndalet serveri, dhe jepjani rrugëve përmes një varësie.
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']}Një test e zëvendëson klientin përmes app.dependency_overrides[get_openemail], kështu që asnjë rrugë nuk arrin te API-ja.
Pika webhook
Verifikoni çdo dërgesë para se të veproni mbi të, me trupin e papërpunuar dhe header-at e kërkesës: await request.body() dhe request.headers në FastAPI, request.body dhe request.headers në Django, dhe request.get_data() dhe request.headers në Flask. Kërkimi i header-ave nuk i dallon shkronjat e mëdha nga të voglat, ndaj objekti i header-ave i secilit framework funksionon ashtu siç është.
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)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 refuzon një POST që nuk mbart token CSRF, dhe një dërgesë nuk mbart asnjë, ndaj view-ja është csrf_exempt: nënshkrimi është ai që provon se kërkesa erdhi nga OpenEmail. Lexoni request.headers në vend të request.META, çelësat e të cilit riemërtohen në formën HTTP_X_OPENEMAIL_SIGNATURE.
Përgjigjuni shpejt me një 2xx dhe bëjeni punën më pas. Një dërgesë që nuk merr përgjigje, ose që merr 408, 425, 429 ose 5xx, provohet sërish, deri në 8 herë brenda rreth 27 orësh e gjysmë, dhe një riluajtje e dërgon sërish një ngjarje me të njëjtin id, ndaj ruani id-të që keni trajtuar dhe kapërceni një përsëritje.
Punët në sfond
Një radhë punësh e ekzekuton sërish një detyrë kur ajo dështon, dhe një detyrë mund të dështojë pasi emaili i saj është nisur: përgjigjja humbi, ose worker-i u ndal para se të mbaronte. Jepni një idempotency_key= të nxjerrë nga ajo që e bëri të nevojshëm dërgimin. Çdo ekzekutim i detyrës mban atëherë të njëjtin çelës, kështu që një përsëritje e riluan mesazhin origjinal në vend që të dërgojë një të dytë.
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']Me acks_late=True, Celery e konfirmon një detyrë vetëm pasi ajo është ekzekutuar, kështu që një detyrë e ndërprerë nga një worker që u ndal mund të dorëzohet sërish, gjë që këtu është e sigurt sepse çelësi e kthen atë ekzekutim të dytë në riluajtje. Retry i RQ-së e ekzekuton sërish një punë të dështuar, me të njëjtin efekt.
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]))Nxirreni çelësin nga ajo që e bëri të nevojshëm dërgimin, kurrë nga një orë. Ai përbëhet nga 1 deri në 255 shkronja, shifra, nënvija, pika, dypika ose viza, ndaj ndërtojeni nga një id dhe jo nga një adresë emaili. Edhe trupin ndërtojeni vetëm nga argumentet e detyrës: një përsëritje me të njëjtin çelës dhe një trup tjetër refuzohet me 422 idempotency_key_reuse në vend që të riluhet.
Ndërtojeni klientin në nivel moduli. Ai nuk hap asnjë lidhje deri në kërkesën e parë, kështu që çdo proces worker i degëzuar nga prindi hap lidhjet e veta.