Files
gy9vin a362ef9f25 refactor(nalogo): восстановить описание чеков из настроек и использовать локальную библиотеку
- Добавлено восстановление описания чека из настроек при обработке очереди
- Передача telegram_user_id и amount_kopeks через всю цепочку создания чеков
- Переход на локальную исправленную версию библ
2025-12-28 04:58:05 +03:00

159 lines
5.2 KiB
Python

"""
Domain exceptions for Moy Nalog API.
Mirrors PHP library's exception hierarchy and error handling.
"""
import logging
import re
from http import HTTPStatus
import httpx
logger = logging.getLogger(__name__)
class DomainException(Exception): # noqa: N818 для совместимости публичного API
"""Base domain exception for all Moy Nalog API errors."""
def __init__(self, message: str, response: httpx.Response | None = None):
super().__init__(message)
self.response = response
# Log the error with response details (without sensitive data)
if response:
self._log_error_details(message, response)
def _log_error_details(self, message: str, response: httpx.Response) -> None:
"""Log error details while avoiding sensitive information."""
# Mask potential sensitive data in URLs and headers
safe_url = self._mask_sensitive_url(str(response.url))
safe_headers = self._mask_sensitive_headers(dict(response.headers))
logger.error(
"API Error: %s | Status: %d | URL: %s | Headers: %s | Body: %s",
message,
response.status_code,
safe_url,
safe_headers,
self._get_safe_response_body(response),
)
def _mask_sensitive_url(self, url: str) -> str:
"""Mask potential sensitive data in URL."""
# Replace tokens/keys with asterisks
patterns = [
(r"(token=)[^&]*", r"\1***"),
(r"(key=)[^&]*", r"\1***"),
(r"(secret=)[^&]*", r"\1***"),
]
for pattern, replacement in patterns:
url = re.sub(pattern, replacement, url)
return url
def _mask_sensitive_headers(self, headers: dict[str, str]) -> dict[str, str]:
"""Mask sensitive headers."""
safe_headers = headers.copy()
sensitive_keys = ["authorization", "x-api-key", "cookie", "set-cookie"]
for key in sensitive_keys:
if key.lower() in [h.lower() for h in safe_headers]:
# Find the actual key (case-insensitive)
actual_key = next(k for k in safe_headers if k.lower() == key.lower())
safe_headers[actual_key] = "***"
return safe_headers
def _get_safe_response_body(self, response: httpx.Response) -> str:
"""Get response body with potential sensitive data masked."""
try:
body = response.text[:1000] # Limit body size for logging
# Mask potential tokens in JSON responses
patterns = [
(r'("token":\s*")[^"]*(")', r"\1***\2"),
(r'("refreshToken":\s*")[^"]*(")', r"\1***\2"),
(r'("password":\s*")[^"]*(")', r"\1***\2"),
(r'("secret":\s*")[^"]*(")', r"\1***\2"),
]
for pattern, replacement in patterns:
body = re.sub(pattern, replacement, body)
return body
except Exception:
return "[Failed to read response body]"
class ValidationException(DomainException):
"""HTTP 400 - Validation error."""
class UnauthorizedException(DomainException):
"""HTTP 401 - Authentication required or invalid credentials."""
class ForbiddenException(DomainException):
"""HTTP 403 - Access forbidden."""
class NotFoundException(DomainException):
"""HTTP 404 - Resource not found."""
class ClientException(DomainException):
"""HTTP 406 - Client error (e.g., wrong Accept headers)."""
class PhoneException(DomainException):
"""HTTP 422 - Phone-related error (SMS, verification, etc.)."""
class ServerException(DomainException):
"""HTTP 500 - Internal server error."""
class UnknownErrorException(DomainException):
"""Unknown HTTP error code."""
def raise_for_status(response: httpx.Response) -> None:
"""
Raise appropriate domain exception based on HTTP status code.
Maps status codes to exceptions exactly like PHP ErrorHandler:
- 400: ValidationException
- 401: UnauthorizedException
- 403: ForbiddenException
- 404: NotFoundException
- 406: ClientException
- 422: PhoneException
- 500: ServerException
- default: UnknownErrorException
Args:
response: httpx.Response object
Raises:
DomainException: Appropriate exception for status code
"""
if response.status_code < HTTPStatus.BAD_REQUEST:
return
body = response.text
if response.status_code == HTTPStatus.BAD_REQUEST:
raise ValidationException(body, response)
if response.status_code == HTTPStatus.UNAUTHORIZED:
raise UnauthorizedException(body, response)
if response.status_code == HTTPStatus.FORBIDDEN:
raise ForbiddenException(body, response)
if response.status_code == HTTPStatus.NOT_FOUND:
raise NotFoundException(body, response)
if response.status_code == HTTPStatus.NOT_ACCEPTABLE:
raise ClientException("Wrong Accept headers", response)
if response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY:
raise PhoneException(body, response)
if response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR:
raise ServerException(body, response)
raise UnknownErrorException(body, response)