Native async clients (0.1.0a1 alpha)
The source tree provides AsyncAlphaSMSClient, AsyncBulkSMSBDClient,
AsyncGreenWebClient, AsyncPathaoClient, AsyncRedXClient and
AsyncSteadfastClient, exported from jukto. These additions have not been
published to PyPI by these tasks. AsyncSSLCommerzClient additionally supports
the scoped hosted payment flow; bKash/Nagad remain planned.
Use the same constructor configuration as the sync client, with an optional
borrowed httpx.AsyncClient. Await send_sms/submit_sms, create_order/
create_shipment, check_status/check_shipment_status, and Pathao's get_token
and get_stores. Results, input validation, error classifications, secure defaults
and unresolved provider contracts match sync clients. Async interfaces are
AsyncBaseSMSProvider and AsyncBaseLogisticsProvider; existing sync interfaces
and calls remain unchanged. There is no hidden event-loop bridge or thread offload.
from jukto import AsyncAlphaSMSClient
async def submit(key: str):
async with AsyncAlphaSMSClient(key) as provider:
return await provider.submit_sms("01700000000", "Synthetic example")
The phone and message above are synthetic; calling this function with real
credentials sends a real request. Routine tests use httpx.MockTransport only.
Lifetime and cancellation
These clients support asyncio, scoped to one event loop and worker. Trio is
not supported by Jukto's asyncio locks and cleanup even though HTTPX supports it.
Do not share providers across loops, processes or sync threads. Reuse one provider
for a worker's lifespan; avoid creating pools inside hot loops. async with or
await provider.aclose() closes an owned pool. Borrowed clients remain open and
must be closed by their owner. Cleanup waits for the owned client's aclose()
even if the cleanup caller is repeatedly cancelled, then propagates cancellation.
A cleanup error remains visible. Finish or cancel and await in-flight tasks
before closing the provider; concurrent shutdown with active requests is unsupported.
asyncio.CancelledError propagates unchanged. Cancellation during a refresh leaves
the previously cached token state intact, releases the token lock and lets another
caller refresh. Fully validated tokens publish atomically without a suspension.
An upstream server may nevertheless rotate a refresh token before cancellation
is observed locally. This cannot be rolled back; invalid-grant fallback follows the
same narrow rule as the sync client. The cache and single-flight lock are per
provider instance, not distributed across workers.
Cancellation after an SMS/shipment request begins may leave a completed upstream
operation. It does not establish failure and does not automatically retry.
Persist an application operation reference before dispatch and reconcile uncertain
outcomes before resubmission. The SDK does not provide durable storage or provider
enforced idempotency. Pathao replays a read once on 401; order replay still requires
retry_unauthorized_orders=True and remains an explicit, potentially duplicating
choice. No generic retries were added.
FastAPI: native requests, bounded concurrency
This application creates one provider at lifespan startup, injects it through a
request dependency, awaits native HTTP and closes it at shutdown. Set
JUKTO_ALPHA_API_KEY, then run uvicorn examples.async_fastapi_app:app from the
checkout after installing the examples extra. Missing configuration fails startup.
Existing sync FastAPI examples remain valid.
"""One native async provider per asyncio worker; bound in-flight submissions."""
from __future__ import annotations
import asyncio
import os
from contextlib import asynccontextmanager
from typing import Any, AsyncIterator, Callable
from fastapi import Depends, FastAPI, Request
from fastapi.responses import JSONResponse
from jukto import AsyncAlphaSMSClient, AsyncBaseSMSProvider, JuktoConfigurationError, JuktoError, SMSBatchError
from examples.application import safe_result
def configured_provider() -> AsyncBaseSMSProvider:
key = os.environ.get('JUKTO_ALPHA_API_KEY', '')
if not key.strip():
raise JuktoConfigurationError('Configure JUKTO_ALPHA_API_KEY before serving requests')
return AsyncAlphaSMSClient(key, timeout=10.0)
def get_provider(request: Request) -> AsyncBaseSMSProvider:
return request.app.state.sms_provider
def create_app(factory: Callable[[], AsyncBaseSMSProvider] = configured_provider,
max_in_flight: int = 10) -> FastAPI:
if type(max_in_flight) is not int or max_in_flight <= 0:
raise ValueError('max_in_flight must be a positive integer')
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
async with factory() as provider:
app.state.sms_provider = provider
app.state.sms_limit = asyncio.Semaphore(max_in_flight)
yield
app = FastAPI(lifespan=lifespan)
@app.post('/sms')
async def submit(request: Request, payload: dict[str, Any],
provider: AsyncBaseSMSProvider = Depends(get_provider)) -> JSONResponse:
if any(not isinstance(payload.get(key), str) or not payload[key].strip() for key in ('phone', 'message')):
return JSONResponse({'error': 'phone and message must be nonempty strings'}, status_code=400)
try:
async with request.app.state.sms_limit:
result = await provider.submit_sms(payload['phone'], payload['message'])
except SMSBatchError as error:
if error.normalized_result is not None:
return JSONResponse(safe_result(error.normalized_result), status_code=202)
return JSONResponse({'error': 'batch_result_unavailable', 'status': 'unknown',
'reconciliation_required': True}, status_code=502)
except JuktoError as error:
return JSONResponse({'error': 'provider_error', 'status': error.outcome.value,
'reconciliation_required': error.reconciliation_required}, status_code=502)
return JSONResponse(safe_result(result), status_code=202)
return app
app = create_app()
max_in_flight limits submissions per worker; HTTPX limits= separately controls
pool connections, and timeout= bounds HTTP phases. A semaphore does not bound
queued requests, aggregate limits across processes, provide rate limiting or cap
total latency. Add ingress limits, queue deadlines, authentication and durable
reconciliation for production. Do not retry the entire GreenWeb batch after a
partial result. Sample error responses omit raw data and recipient details.
Evidence and verification
Implementation follows HTTPX async client and lifecycle guidance (checked 2026-10-10). No new provider code, endpoint, success or idempotency claim was introduced; the provider matrix applies equally to both interfaces. Offline tests compare serialized requests, normalized results and error metadata across all six providers, exercise AlphaSMS's recorded official codes, malformed responses, partial outcomes, concurrent refresh, cancellation, client ownership and bounded FastAPI requests. Mocks establish SDK behavior, not live provider compatibility.