Добавлена отправка накладных на почту
This commit is contained in:
@@ -253,6 +253,55 @@ class CDEKClient:
|
||||
"CDEK get waybill response payload is invalid."
|
||||
) from exc
|
||||
|
||||
async def download_waybill_pdf(self, url: str) -> bytes:
|
||||
failure_message = "CDEK waybill PDF download failed"
|
||||
for attempt in range(self._retry_attempts + 1):
|
||||
try:
|
||||
token = await self._auth_client.get_access_token()
|
||||
response = await self._http_client.get(
|
||||
url,
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=self._timeout_seconds,
|
||||
)
|
||||
except (httpx.TimeoutException, httpx.TransportError) as exc:
|
||||
if attempt < self._retry_attempts:
|
||||
await self._sleep(self._retry_delay(attempt))
|
||||
continue
|
||||
raise CDEKClientError(
|
||||
f"{failure_message} after retry attempts."
|
||||
) from exc
|
||||
|
||||
if self._should_retry(response.status_code):
|
||||
if attempt < self._retry_attempts:
|
||||
await self._sleep(self._retry_delay(attempt))
|
||||
continue
|
||||
raise CDEKClientError(
|
||||
f"{failure_message} with retriable status "
|
||||
f"{response.status_code}."
|
||||
)
|
||||
|
||||
if 400 <= response.status_code < 500:
|
||||
log.warning(
|
||||
"cdek_waybill_pdf_download_rejected",
|
||||
url=url,
|
||||
status_code=response.status_code,
|
||||
response_body=_response_text_or_none(response),
|
||||
)
|
||||
raise CDEKRequestError(
|
||||
f"{failure_message} with status {response.status_code}."
|
||||
)
|
||||
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except httpx.HTTPError as exc:
|
||||
raise CDEKClientError(
|
||||
f"{failure_message}: invalid response."
|
||||
) from exc
|
||||
|
||||
return response.content
|
||||
|
||||
raise CDEKClientError(f"{failure_message} unexpectedly.")
|
||||
|
||||
async def _get_json(self, url: str, *, failure_message: str) -> dict[str, Any]:
|
||||
for attempt in range(self._retry_attempts + 1):
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""E-mail adapters."""
|
||||
|
||||
from app.adapters.email.smtp_client import SMTPEmailSender, SMTPEmailSenderError
|
||||
|
||||
__all__ = ["SMTPEmailSender", "SMTPEmailSenderError"]
|
||||
@@ -0,0 +1,135 @@
|
||||
"""SMTP e-mail adapter built on top of aiosmtplib."""
|
||||
|
||||
from email.message import EmailMessage
|
||||
from typing import Protocol
|
||||
|
||||
import aiosmtplib
|
||||
import structlog
|
||||
|
||||
log = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class SMTPEmailSenderError(Exception):
|
||||
"""Raised when the SMTP delivery fails."""
|
||||
|
||||
|
||||
class SMTPSendFn(Protocol):
|
||||
async def __call__(
|
||||
self,
|
||||
message: EmailMessage,
|
||||
*,
|
||||
hostname: str,
|
||||
port: int,
|
||||
username: str | None,
|
||||
password: str | None,
|
||||
use_tls: bool,
|
||||
start_tls: bool,
|
||||
timeout: float,
|
||||
) -> object: ...
|
||||
|
||||
|
||||
class SMTPEmailSender:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
smtp_host: str,
|
||||
smtp_port: int,
|
||||
username: str,
|
||||
password: str,
|
||||
from_address: str,
|
||||
use_tls: bool = True,
|
||||
timeout_seconds: float = 10.0,
|
||||
send: SMTPSendFn | None = None,
|
||||
) -> None:
|
||||
self._smtp_host = smtp_host
|
||||
self._smtp_port = smtp_port
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._from_address = from_address
|
||||
self._use_tls = use_tls
|
||||
self._timeout_seconds = timeout_seconds
|
||||
self._send = send or _default_send
|
||||
|
||||
async def send_email(
|
||||
self,
|
||||
*,
|
||||
to: str,
|
||||
subject: str,
|
||||
body: str,
|
||||
attachment_bytes: bytes,
|
||||
attachment_filename: str,
|
||||
) -> None:
|
||||
message = self._build_message(
|
||||
to=to,
|
||||
subject=subject,
|
||||
body=body,
|
||||
attachment_bytes=attachment_bytes,
|
||||
attachment_filename=attachment_filename,
|
||||
)
|
||||
try:
|
||||
await self._send(
|
||||
message,
|
||||
hostname=self._smtp_host,
|
||||
port=self._smtp_port,
|
||||
username=self._username or None,
|
||||
password=self._password or None,
|
||||
use_tls=self._use_tls and self._smtp_port == 465,
|
||||
start_tls=self._use_tls and self._smtp_port != 465,
|
||||
timeout=self._timeout_seconds,
|
||||
)
|
||||
except Exception as exc:
|
||||
log.warning(
|
||||
"smtp_send_failed",
|
||||
smtp_host=self._smtp_host,
|
||||
smtp_port=self._smtp_port,
|
||||
to=to,
|
||||
error=str(exc),
|
||||
)
|
||||
raise SMTPEmailSenderError(
|
||||
f"SMTP send to {to} failed: {exc}"
|
||||
) from exc
|
||||
|
||||
def _build_message(
|
||||
self,
|
||||
*,
|
||||
to: str,
|
||||
subject: str,
|
||||
body: str,
|
||||
attachment_bytes: bytes,
|
||||
attachment_filename: str,
|
||||
) -> EmailMessage:
|
||||
message = EmailMessage()
|
||||
message["From"] = self._from_address
|
||||
message["To"] = to
|
||||
message["Subject"] = subject
|
||||
message.set_content(body)
|
||||
message.add_attachment(
|
||||
attachment_bytes,
|
||||
maintype="application",
|
||||
subtype="pdf",
|
||||
filename=attachment_filename,
|
||||
)
|
||||
return message
|
||||
|
||||
|
||||
async def _default_send(
|
||||
message: EmailMessage,
|
||||
*,
|
||||
hostname: str,
|
||||
port: int,
|
||||
username: str | None,
|
||||
password: str | None,
|
||||
use_tls: bool,
|
||||
start_tls: bool,
|
||||
timeout: float,
|
||||
) -> object:
|
||||
return await aiosmtplib.send(
|
||||
message,
|
||||
hostname=hostname,
|
||||
port=port,
|
||||
username=username,
|
||||
password=password,
|
||||
use_tls=use_tls,
|
||||
start_tls=start_tls,
|
||||
timeout=timeout,
|
||||
)
|
||||
Reference in New Issue
Block a user