Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[MESSAGES CONTROL]
disable=
C0116,
C0301,
C0325,
C0415,
R0801,
R0903,
R0917
45 changes: 45 additions & 0 deletions mercadopago/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,53 @@
payment = sdk.payment().create({...})
"""
from mercadopago.sdk import SDK
from mercadopago.errors.exceptions import (
MercadoPagoError,
MPBadRequestError,
MPAuthenticationError,
MPPaymentError,
MPForbiddenError,
MPNotFoundError,
MPIdempotencyError,
MPValidationError,
MPResourceLockedError,
MPDependencyError,
MPRateLimitError,
MPServerError,
MPConnectionError,
)
from mercadopago.errors.constants import MPOrderErrors, MPPaymentErrors
from mercadopago.errors.response import MPResponse
from mercadopago.resources.status import (
PaymentStatus,
OrderStatus,
PreapprovalStatus,
MerchantOrderStatus,
RefundStatus,
)


__all__ = (
'SDK',
'MercadoPagoError',
'MPBadRequestError',
'MPAuthenticationError',
'MPPaymentError',
'MPForbiddenError',
'MPNotFoundError',
'MPIdempotencyError',
'MPValidationError',
'MPResourceLockedError',
'MPDependencyError',
'MPRateLimitError',
'MPServerError',
'MPConnectionError',
'MPOrderErrors',
'MPPaymentErrors',
'MPResponse',
'PaymentStatus',
'OrderStatus',
'PreapprovalStatus',
'MerchantOrderStatus',
'RefundStatus',
)
18 changes: 18 additions & 0 deletions mercadopago/config/defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Default configuration constants for the MercadoPago Python SDK.

These match the current SDK behaviour so that callers who never set retry
options receive identical results to before.

Attributes:
DEFAULT_TIMEOUT_SECONDS: Request timeout in seconds (60.0).
DEFAULT_MAX_RETRIES: Maximum automatic retries on transient errors (3).
DEFAULT_RETRY_ON: HTTP status codes that trigger a retry.
DEFAULT_MAX_DELAY: Maximum delay between retries in milliseconds (30 000).
DEFAULT_INITIAL_DELAY: Initial backoff delay in milliseconds (None = urllib3 default).
"""

DEFAULT_TIMEOUT_SECONDS = 60.0
DEFAULT_MAX_RETRIES = 3
DEFAULT_RETRY_ON = [429, 500, 502, 503, 504]
DEFAULT_MAX_DELAY = 30_000 # milliseconds
DEFAULT_INITIAL_DELAY = None # None = no extra backoff beyond urllib3 default
92 changes: 90 additions & 2 deletions mercadopago/config/request_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
import uuid

from .config import Config
from .defaults import (
DEFAULT_TIMEOUT_SECONDS,
DEFAULT_MAX_RETRIES,
DEFAULT_MAX_DELAY,
DEFAULT_INITIAL_DELAY,
)


class RequestOptions: # pylint: disable=too-many-instance-attributes
Expand Down Expand Up @@ -33,17 +39,27 @@ class RequestOptions: # pylint: disable=too-many-instance-attributes
__corporation_id = None
__integrator_id = None
__platform_id = None
__initial_delay = None
__max_delay = None
__jitter = None
__retry_on = None
__on_retry = None

def __init__( # pylint: disable=too-many-positional-arguments
# pylint: disable=too-many-arguments
self,
access_token=None,
connection_timeout=60.0,
connection_timeout=DEFAULT_TIMEOUT_SECONDS,
custom_headers=None,
corporation_id=None,
integrator_id=None,
platform_id=None,
max_retries=3,
max_retries=DEFAULT_MAX_RETRIES,
initial_delay=DEFAULT_INITIAL_DELAY,
max_delay=DEFAULT_MAX_DELAY,
jitter=None,
retry_on=None,
on_retry=None,
):
"""Initialises request options with sensible defaults.

Expand Down Expand Up @@ -79,6 +95,11 @@ def __init__( # pylint: disable=too-many-positional-arguments
self.integrator_id = integrator_id
if platform_id is not None:
self.platform_id = platform_id
self.__initial_delay = initial_delay
self.__max_delay = max_delay
self.__jitter = jitter
self.__retry_on = retry_on
self.__on_retry = on_retry

self.__config = Config()

Expand Down Expand Up @@ -155,6 +176,14 @@ def custom_headers(self):
def custom_headers(self, value):
if not isinstance(value, dict):
raise ValueError("Param custom_headers must be a Dictionary")
idem_key = value.get("x-idempotency-key")
if idem_key is None:
idem_key = value.get("X-Idempotency-Key")
if idem_key is not None and not (1 <= len(str(idem_key)) <= 64):
raise ValueError(
"x-idempotency-key must be between 1 and 64 characters "
f"(got {len(str(idem_key))})"
)
self.__custom_headers = value

@property
Expand Down Expand Up @@ -189,3 +218,62 @@ def platform_id(self, value):
if not isinstance(value, str):
raise ValueError("Param platform_id must be a String")
self.__platform_id = value

@property
def initial_delay(self):
"""Initial backoff delay in ms (None = no extra delay)."""
return self.__initial_delay

@initial_delay.setter
def initial_delay(self, value):
if value is not None and not isinstance(value, int):
raise ValueError("Param initial_delay must be an Integer or None")
self.__initial_delay = value

@property
def max_delay(self):
"""Maximum backoff delay cap in ms."""
return self.__max_delay

@max_delay.setter
def max_delay(self, value):
if value is not None and not isinstance(value, int):
raise ValueError("Param max_delay must be an Integer or None")
self.__max_delay = value

@property
def jitter(self):
"""Whether to add random jitter to retry delay."""
return self.__jitter

@jitter.setter
def jitter(self, value):
if value is not None and not isinstance(value, bool):
raise ValueError("Param jitter must be a Boolean or None")
self.__jitter = value

@property
def retry_on(self):
"""List of HTTP status codes that trigger a retry, or None for defaults."""
return self.__retry_on

@retry_on.setter
def retry_on(self, value):
if value is not None:
if not isinstance(value, list):
raise ValueError("Param retry_on must be a List or None")
for code in value:
if not isinstance(code, int) or code < 100 or code > 599:
raise ValueError(f"retry_on contains invalid HTTP status code: {code}")
self.__retry_on = value

@property
def on_retry(self):
"""Optional callback(attempt, error) invoked before each retry."""
return self.__on_retry

@on_retry.setter
def on_retry(self, value):
if value is not None and not callable(value):
raise ValueError("Param on_retry must be callable or None")
self.__on_retry = value
33 changes: 14 additions & 19 deletions mercadopago/core/mp_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from mercadopago.config.config import Config
from mercadopago.config.request_options import RequestOptions
from mercadopago.errors.response import MPResponse


class MPBase:
Expand Down Expand Up @@ -105,13 +106,14 @@ def _get(self, uri, filters=None, request_options=None):
headers = self.__check_headers(
request_options, {"Content-type": self.__config.mime_json})

return self.__http_client.get(
return MPResponse(self.__http_client.get(
url=self.__config.api_base_url + uri,
params=filters,
headers=headers,
timeout=request_options.connection_timeout,
maxretries=request_options.max_retries,
)
retry_on=request_options.retry_on,
))

def _post(self, uri, data=None, params=None, request_options=None):
"""Performs an authenticated POST request.
Expand All @@ -132,42 +134,34 @@ def _post(self, uri, data=None, params=None, request_options=None):
headers = self.__check_headers(
request_options, {"Content-type": self.__config.mime_json})

return self.__http_client.post(
return MPResponse(self.__http_client.post(
url=self.__config.api_base_url + uri,
data=data,
params=params,
headers=headers,
timeout=request_options.connection_timeout,
maxretries=request_options.max_retries,
)
retry_on=request_options.retry_on,
))

def _put(self, uri, data=None, params=None, request_options=None):
"""Performs an authenticated PUT request.

Args:
uri: API path relative to the base URL.
data: Request body dict; JSON-encoded automatically.
params: Optional query-string parameters.
request_options: Per-call overrides; falls back to instance defaults.

Returns:
dict: ``{"status": <http_code>, "response": <parsed_json>}``.
"""
"""Performs an authenticated PUT request."""
if data is not None:
data = JSONEncoder().encode(data)

request_options = self.__check_request_options(request_options)
headers = self.__check_headers(
request_options, {"Content-type": self.__config.mime_json})

return self.__http_client.put(
return MPResponse(self.__http_client.put(
url=self.__config.api_base_url + uri,
data=data,
params=params,
headers=headers,
timeout=request_options.connection_timeout,
maxretries=request_options.max_retries,
)
retry_on=request_options.retry_on,
))

def _delete(self, uri, params=None, request_options=None):
"""Performs an authenticated DELETE request.
Expand All @@ -183,13 +177,14 @@ def _delete(self, uri, params=None, request_options=None):
request_options = self.__check_request_options(request_options)
headers = self.__check_headers(request_options)

return self.__http_client.delete(
return MPResponse(self.__http_client.delete(
url=self.__config.api_base_url + uri,
params=params,
headers=headers,
timeout=request_options.connection_timeout,
maxretries=request_options.max_retries,
)
retry_on=request_options.retry_on,
))

@property
def request_options(self):
Expand Down
36 changes: 36 additions & 0 deletions mercadopago/errors/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Errors package for the MercadoPago Python SDK."""
from .exceptions import (
MercadoPagoError,
MPBadRequestError,
MPAuthenticationError,
MPPaymentError,
MPForbiddenError,
MPNotFoundError,
MPIdempotencyError,
MPValidationError,
MPResourceLockedError,
MPDependencyError,
MPRateLimitError,
MPServerError,
MPConnectionError,
build_error,
)
from .response import MPResponse

__all__ = [
"MercadoPagoError",
"MPBadRequestError",
"MPAuthenticationError",
"MPPaymentError",
"MPForbiddenError",
"MPNotFoundError",
"MPIdempotencyError",
"MPValidationError",
"MPResourceLockedError",
"MPDependencyError",
"MPRateLimitError",
"MPServerError",
"MPConnectionError",
"build_error",
"MPResponse",
]
27 changes: 27 additions & 0 deletions mercadopago/errors/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Error string constants for MercadoPago API error codes.

Use these constants instead of hard-coding ``error`` string literals so
that code is refactoring-safe and benefits from IDE auto-complete.

Example::

from mercadopago.errors.constants import MPOrderErrors
except MPIdempotencyError as e:
if e.error == MPOrderErrors.CANNOT_REFUND:
handle_cannot_refund()
"""


class MPOrderErrors:
"""Machine-readable ``error`` strings returned for Order/v1 conflicts (HTTP 409)."""
CANNOT_REFUND = "cannot_refund_order"
CANNOT_CANCEL = "cannot_cancel_order"
CANNOT_CAPTURE = "cannot_capture_order"
ALREADY_REFUNDED = "order_already_refunded"
ALREADY_CANCELED = "order_already_canceled"
RESOURCE_LOCKED = "resource_locked"


class MPPaymentErrors:
"""Machine-readable ``error`` strings returned for payment errors (HTTP 402)."""
FAILED = "failed"
Loading