class RequestError(Error):
    """Errors while preparing or performing an API request."""

    pass class RequestSetupError(RequestError):
    """Errors while preparing an API request."""

    pass class Captures(ResourceBase):
    RESOURCE_ID_PREFIX = "cpt_"

    def get_resource_object(self, result):
        return Capture(result, self.client)

    def get_resource_name(self):
        return f"payments/{self.payment_id}/captures"

    def get(self, capture_id, **params):
        """Verify the capture ID and retrieve the capture from the API."""
        if not capture_id or not capture_id.startswith(self.RESOURCE_ID_PREFIX):
            raise IdentifierError(
                f"Invalid capture ID: '{capture_id}'. A capture ID should start with '{self.RESOURCE_ID_PREFIX}'."
            )
        return super().get(capture_id, **params)

    def with_parent_id(self, payment_id):
        self.payment_id = payment_id
        return self

    def on(self, payment):
        return self.with_parent_id(payment.id) class Chargebacks(ResourceBase):
    RESOURCE_ID_PREFIX = "chb_"

    def get_resource_object(self, result):
        return Chargeback(result, self.client)

    def get(self, chargeback_id, **params):
        """Verify the chargeback ID and retrieve the chargeback from the API."""
        if not chargeback_id or not chargeback_id.startswith(self.RESOURCE_ID_PREFIX):
            raise IdentifierError(
                f"Invalid chargeback ID: '{chargeback_id}'. "
                f"A chargeback ID should start with '{self.RESOURCE_ID_PREFIX}'."
            )
        return super().get(chargeback_id, **params) class CustomerMandates(ResourceBase):
    RESOURCE_ID_PREFIX = "mdt_"
    customer_id = None

    def get_resource_object(self, result):
        return Mandate(result, self.client)

    def get(self, mandate_id, **params):
        if not mandate_id or not mandate_id.startswith(self.RESOURCE_ID_PREFIX):
            raise IdentifierError(
                f"Invalid mandate ID: '{mandate_id}'. A mandate ID should start with '{self.RESOURCE_ID_PREFIX}'."
            )
        return super().get(mandate_id, **params)

    def get_resource_name(self):
        return f"customers/{self.customer_id}/mandates"

    def with_parent_id(self, customer_id):
        self.customer_id = customer_id
        return self

    def on(self, customer):
        return self.with_parent_id(customer.id) class CustomerPayments(Payments):
    customer_id = None

    def get_resource_name(self):
        return f"customers/{self.customer_id}/payments"

    def with_parent_id(self, customer_id):
        self.customer_id = customer_id
        return self

    def on(self, customer):
        return self.with_parent_id(customer.id) class CustomerSubscriptions(ResourceBase):
    RESOURCE_ID_PREFIX = "sub_"
    customer_id = None

    def get_resource_object(self, result):
        return Subscription(result, self.client)

    def get(self, subscription_id, **params):
        if not subscription_id or not subscription_id.startswith(self.RESOURCE_ID_PREFIX):
            raise IdentifierError(
                f"Invalid subscription ID: '{subscription_id}'. A subscription ID "
                f"should start with '{self.RESOURCE_ID_PREFIX}'."
            )
        return super().get(subscription_id, **params)

    def delete(self, subscription_id, data=None):
        """Cancel subscription and return the subscription object.

        Deleting a subscription causes the subscription status to changed to 'canceled'.
        The updated subscription object is returned.
        """
        if not subscription_id or not subscription_id.startswith(self.RESOURCE_ID_PREFIX):
            raise IdentifierError(
                f"Invalid subscription ID: '{subscription_id}'. A subscription ID "
                f"should start with '{self.RESOURCE_ID_PREFIX}'."
            )
        result = super().delete(subscription_id, data)
        return self.get_resource_object(result)

    def get_resource_name(self):
        return f"customers/{self.customer_id}/subscriptions"

    def with_parent_id(self, customer_id):
        self.customer_id = customer_id
        return self

    def on(self, customer):
        return self.with_parent_id(customer.id) class Customers(ResourceBase):
    RESOURCE_ID_PREFIX = "cst_"

    def get_resource_object(self, result):
        return Customer(result, self.client)

    def get(self, customer_id, **params):
        if not customer_id or not customer_id.startswith(self.RESOURCE_ID_PREFIX):
            raise IdentifierError(
                f"Invalid customer ID: '{customer_id}'. A customer ID should start with '{self.RESOURCE_ID_PREFIX}'."
            )
        return super().get(customer_id, **params) class Invoices(ResourceBase):
    RESOURCE_ID_PREFIX = "inv_"

    def get_resource_object(self, result):
        return Invoice(result, self.client)

    def get(self, invoice_id, **params):
        if not invoice_id or not invoice_id.startswith(self.RESOURCE_ID_PREFIX):
            raise IdentifierError(
                f"Invalid invoice ID: '{invoice_id}'. An invoice ID should start with '{self.RESOURCE_ID_PREFIX}'."
            )
        return super().get(invoice_id, **params) class Methods(ResourceBase):
    def get_resource_object(self, result):
        return Method(result)

    def all(self, **params):
        """List all mollie payment methods, including methods that aren't activated in your profile."""
        path = "methods/all"
        result = self.perform_api_call(self.REST_LIST, path, params=params)
        return ObjectList(result, self.get_resource_object({}).__class__, self.client) class Onboarding(ResourceBase):
    def get_resource_object(self, result):
        return OnboardingObject(result, self.client)

    def get(self, onboarding_id, **params):
        if not onboarding_id or not onboarding_id == "me":
            raise IdentifierError(f"Invalid onboarding ID: '{onboarding_id}'. The onboarding ID should be 'me'.")
        return super().get(onboarding_id, **params)

    def create(self, resource_id, data=None, **params):
        path = self.get_resource_name() + "/" + str(resource_id)
        result = self.perform_api_call(self.REST_CREATE, path, data, params)
        return self.get_resource_object(result) class Orders(ResourceBase):
    RESOURCE_ID_PREFIX = "ord_"

    def get_resource_object(self, result):
        return Order(result, self.client)

    def get(self, order_id, **params):
        if not order_id or not order_id.startswith(self.RESOURCE_ID_PREFIX):
            raise IdentifierError(
                f"Invalid order ID: '{order_id}'. An order ID should start with '{self.RESOURCE_ID_PREFIX}'."
            )

        result_order = super().get(order_id, **params)

        requested_embeds = self.extract_embed(params)
        if requested_embeds:
            result_order.requested_embeds = requested_embeds

        return result_order

    def delete(self, order_id, data=None):
        """Cancel order and return the order object.

        Deleting an order causes the order status to change to canceled.
        The updated order object is returned.
        """
        if not order_id or not order_id.startswith(self.RESOURCE_ID_PREFIX):
            raise IdentifierError(
                f"Invalid order ID: '{order_id}'. An order ID should start with '{self.RESOURCE_ID_PREFIX}'."
            )
        result = super().delete(order_id, data)
        return self.get_resource_object(result) class Organizations(ResourceBase):
    RESOURCE_ID_PREFIX = "org_"

    def get_resource_object(self, result):
        return Organization(result, self.client)

    def get(self, organization_id, **params):
        if not organization_id or (
            not organization_id.startswith(self.RESOURCE_ID_PREFIX) and not organization_id == "me"
        ):
            raise IdentifierError(
                f"Invalid organization ID: '{organization_id}'. A organization ID should start "
                f"with '{self.RESOURCE_ID_PREFIX}' or it should be 'me'."
            )
        return super().get(organization_id, **params) class PaymentChargebacks(Chargebacks):
    payment_id = None

    def get_resource_name(self):
        return f"payments/{self.payment_id}/chargebacks"

    def with_parent_id(self, payment_id):
        self.payment_id = payment_id
        return self

    def on(self, payment):
        return self.with_parent_id(payment.id) class PaymentLinks(ResourceBase):
    RESOURCE_ID_PREFIX = "pl_"

    def get_resource_object(self, result):
        return PaymentLink(result, self.client)

    def get_resource_name(self):
        return "payment-links"

    def get(self, payment_link_id, **params):
        if not payment_link_id or not payment_link_id.startswith(self.RESOURCE_ID_PREFIX):
            raise IdentifierError(
                f"Invalid payment ID: '{payment_link_id}'. A payment ID should start with '{self.RESOURCE_ID_PREFIX}'."
            )
        return super().get(payment_link_id, **params) class PaymentRefunds(Refunds):
    payment_id = None

    def get_resource_name(self):
        return f"payments/{self.payment_id}/refunds"

    def with_parent_id(self, payment_id):
        self.payment_id = payment_id
        return self

    def on(self, payment):
        return self.with_parent_id(payment.id) class Payments(ResourceBase):
    RESOURCE_ID_PREFIX = "tr_"

    def get_resource_object(self, result):
        return Payment(result, self.client)

    def get(self, payment_id, **params):
        if not payment_id or not payment_id.startswith(self.RESOURCE_ID_PREFIX):
            raise IdentifierError(
                f"Invalid payment ID: '{payment_id}'. A payment ID should start with '{self.RESOURCE_ID_PREFIX}'."
            )
        return super().get(payment_id, **params)

    def delete(self, payment_id, data=None):
        """Cancel payment and return the payment object.

        Deleting a payment causes the payment status to change to canceled.
        The updated payment object is returned.
        """
        if not payment_id or not payment_id.startswith(self.RESOURCE_ID_PREFIX):
            raise IdentifierError(
                f"Invalid payment ID: '{payment_id}'. A payment ID should start with '{self.RESOURCE_ID_PREFIX}'."
            )
        result = super().delete(payment_id, data)
        return self.get_resource_object(result) class Permissions(ResourceBase):
    def get_resource_object(self, result):
        return Permission(result, self.client)

    @staticmethod
    def validate_permission_id(permission_id):
        if not permission_id or not bool(re.match(r"^[a-z]+\.[a-z]+$", permission_id)):
            raise IdentifierError(f"Invalid permission ID: '{permission_id}'. Does not match ^[a-z]+.[a-z]+$")

    def get(self, permission_id, **params):
        self.validate_permission_id(permission_id)
        return super().get(permission_id, **params) class ProfileChargebacks(Chargebacks):
    profile_id = None

    def get_resource_name(self):
        return f"chargebacks?profileId={self.profile_id}"

    def with_parent_id(self, profile_id):
        self.profile_id = profile_id
        return self

    def on(self, profile):
        return self.with_parent_id(profile.id) class ProfileMethods(Methods):
    RESOURCE_REQUIRED_METHODS = ["giftcard", "voucher"]

    profile_id = None
    method_id = None
    resource_id = None

    def get_resource_name(self):
        path = f"profiles/{self.profile_id}/methods/{self.method_id}"
        if self.method_id in self.RESOURCE_REQUIRED_METHODS:
            path += f"/issuers/{self.resource_id}"
        return path

    def list(self, **params):
        return self.client.methods.list(profileId=self.profile_id)

    def delete(self, resource_id=None, *args, **kwargs):
        if self.method_id in self.RESOURCE_REQUIRED_METHODS and resource_id is None:
            raise RequestError(f"resource_id is required when disabling a {self.method_id}.")
        self.resource_id = resource_id
        path = self.get_resource_name()
        return self.perform_api_call(self.REST_DELETE, path, None)

    def create(self, resource_id=None, data=None, **params):
        if self.method_id in self.RESOURCE_REQUIRED_METHODS and resource_id is None:
            raise RequestError(f"resource_id is required when enabling a {self.method_id}.")
        self.resource_id = resource_id
        path = self.get_resource_name()
        result = self.perform_api_call(self.REST_CREATE, path, data=data, params=params)
        return self.get_resource_object(result)

    def with_parent_id(self, profile_id, method=None):
        self.method_id = method
        self.profile_id = profile_id
        return self

    def on(self, profile, method=None):
        return self.with_parent_id(profile.id, method) class ProfilePayments(Payments):
    profile_id = None

    def get_resource_name(self):
        return f"payments?profileId={self.profile_id}"

    def with_parent_id(self, profile_id):
        self.profile_id = profile_id
        return self

    def on(self, profile):
        return self.with_parent_id(profile.id) class ProfileRefunds(Refunds):
    profile_id = None

    def get_resource_name(self):
        return f"refunds?profileId={self.profile_id}"

    def with_parent_id(self, profile_id):
        self.profile_id = profile_id
        return self

    def on(self, profile):
        return self.with_parent_id(profile.id) class Profiles(ResourceBase):
    RESOURCE_ID_PREFIX = "pfl_"

    def get_resource_object(self, result):
        return Profile(result, self.client)

    def get(self, profile_id, **params):
        if not profile_id or (not profile_id.startswith(self.RESOURCE_ID_PREFIX) and not profile_id == "me"):
            raise IdentifierError(
                f"Invalid profile ID: '{profile_id}'. A profile ID should start with '{self.RESOURCE_ID_PREFIX}' "
                "or it should be 'me'."
            )
        return super().get(profile_id, **params) class Refunds(ResourceBase):
    RESOURCE_ID_PREFIX = "re_"

    def get_resource_object(self, result):
        return Refund(result, self.client)

    def get(self, refund_id, **params):
        if not refund_id or not refund_id.startswith(self.RESOURCE_ID_PREFIX):
            raise IdentifierError(
                f"Invalid refund ID: '{refund_id}'. A refund ID should start with '{self.RESOURCE_ID_PREFIX}'."
            )
        return super().get(refund_id, **params) class SettlementCaptures(Captures):
    settlement_id = None

    def get_resource_name(self):
        return f"settlements/{self.settlement_id}/captures"

    def with_parent_id(self, settlement_id):
        self.settlement_id = settlement_id
        return self

    def on(self, settlement):
        return self.with_parent_id(settlement.id) class SettlementChargebacks(Chargebacks):
    settlement_id = None

    def get_resource_name(self):
        return f"settlements/{self.settlement_id}/chargebacks"

    def with_parent_id(self, settlement_id):
        self.settlement_id = settlement_id
        return self

    def on(self, settlement):
        return self.with_parent_id(settlement.id) class SettlementPayments(Payments):
    settlement_id = None

    def get_resource_name(self):
        return f"settlements/{self.settlement_id}/payments"

    def with_parent_id(self, settlement_id):
        self.settlement_id = settlement_id
        return self

    def on(self, settlement):
        return self.with_parent_id(settlement.id) class SettlementRefunds(Refunds):
    settlement_id = None

    def get_resource_name(self):
        return f"settlements/{self.settlement_id}/refunds"

    def with_parent_id(self, settlement_id):
        self.settlement_id = settlement_id
        return self

    def on(self, settlement):
        return self.with_parent_id(settlement.id) class Settlements(ResourceBase):
    RESOURCE_ID_PREFIX = "stl_"

    # According to Mollie, the bank reference is formatted as:
    # - The Mollie customer ID, 4 to 7 digits.
    # - The year and month, 4 digits
    # - The sequence number of the settlement in that month, 2 digits
    # The components are separated by a dot.
    BANK_REFERENCE_REGEX = re.compile(r"^\d{4,7}\.\d{4}\.\d{2}$", re.ASCII)

    def get_resource_object(self, result):
        return Settlement(result, self.client)

    @classmethod
    def validate_settlement_id(cls, settlement_id):
        if not settlement_id or (
            not settlement_id.startswith(cls.RESOURCE_ID_PREFIX)
            and settlement_id not in ["next", "open"]
            and not cls.BANK_REFERENCE_REGEX.match(settlement_id)
        ):
            raise IdentifierError(
                f"Invalid settlement ID: '{settlement_id}'. A settlement ID should "
                f"start with '{cls.RESOURCE_ID_PREFIX}' "
                ", be 'next' or 'open' or contain a valid bank reference."
            )

    def get(self, settlement_id, **params):
        self.validate_settlement_id(settlement_id)
        """Verify the settlement ID and retrieve the settlement from the API."""
        return super().get(settlement_id, **params) class Shipments(ResourceBase):
    order_id = None

    def get_resource_object(self, result):
        return Shipment(result, self.client)

    def get_resource_name(self):
        return f"orders/{self.order_id}/shipments"

    def with_parent_id(self, order_id):
        self.order_id = order_id
        return self

    def on(self, order):
        return self.with_parent_id(order.id) class SubscriptionPayments(Payments):
    customer_id = None
    subscription_id = None

    def get_resource_name(self):
        return f"customers/{self.customer_id}/subscriptions/{self.subscription_id}/payments"

    def with_parent_id(self, customer_id, subscription_id):
        self.customer_id = customer_id
        self.subscription_id = subscription_id
        return self

    def on(self, subscription):
        # TODO: A request has been filed to mollie to add the customer id to the subscription response.
        return self.with_parent_id(subscription.customer.id, subscription.id) class Subscriptions(ResourceBase):
    def get_resource_object(self, result):
        return Subscription(result, self.client)

    def create(self, data=None, **params):
        raise NotImplementedError('The endpoint "create" is not supported.')

    def get(self, resource_id, **params):
        raise NotImplementedError('The endpoint "get" is not supported.')

    def update(self, resource_id, data=None, **params):
        raise NotImplementedError('The endpoint "update" is not supported.')

    def delete(self, resource_id, data=None):
        raise NotImplementedError('The endpoint "delete" is not supported.') VERSION = "2.12.0"
import json
import platform
import re
import ssl
import requests
from collections import OrderedDict
from urllib.parse import urlencode
from requests_oauthlib import OAuth2Session
from urllib3.util import Retry
from .error import RequestError, RequestSetupError
from .resources.captures import Captures
from .resources.chargebacks import Chargebacks
from .resources.customer_mandates import CustomerMandates
from .resources.customer_payments import CustomerPayments
from .resources.customer_subscriptions import CustomerSubscriptions
from .resources.customers import Customers
from .resources.invoices import Invoices
from .resources.methods import Methods
from .resources.onboarding import Onboarding
from .resources.orders import Orders
from .resources.organizations import Organizations
from .resources.payment_chargebacks import PaymentChargebacks
from .resources.payment_links import PaymentLinks
from .resources.payment_refunds import PaymentRefunds
from .resources.payments import Payments
from .resources.permissions import Permissions
from .resources.profile_chargebacks import ProfileChargebacks
from .resources.profile_methods import ProfileMethods
from .resources.profile_payments import ProfilePayments
from .resources.profile_refunds import ProfileRefunds
from .resources.profiles import Profiles
from .resources.refunds import Refunds
from .resources.settlement_captures import SettlementCaptures
from .resources.settlement_chargebacks import SettlementChargebacks
from .resources.settlement_payments import SettlementPayments
from .resources.settlement_refunds import SettlementRefunds
from .resources.settlements import Settlements
from .resources.shipments import Shipments
from .resources.subscription_payments import SubscriptionPayments
from .resources.subscriptions import Subscriptions
from .version import VERSION




class Client(object):
    CLIENT_VERSION = VERSION
    API_ENDPOINT = "https://api.mollie.com"
    API_VERSION = "v2"
    UNAME = " ".join(platform.uname())

    OAUTH_AUTHORIZATION_URL = "https://www.mollie.com/oauth2/authorize"
    OAUTH_AUTO_REFRESH_URL = API_ENDPOINT + "/oauth2/tokens"
    OAUTH_TOKEN_URL = API_ENDPOINT + "/oauth2/tokens"

    @staticmethod
    def validate_api_endpoint(api_endpoint):
        return api_endpoint.strip().rstrip("/")

    @staticmethod
    def validate_api_key(api_key):
        api_key = api_key.strip()
        if not re.compile(r"^(live|test)_\w+$").match(api_key):
            raise RequestSetupError(f"Invalid API key: '{api_key}'. An API key must start with 'test_' or 'live_'.")
        return api_key

    @staticmethod
    def validate_access_token(access_token):
        access_token = access_token.strip()
        if not access_token.startswith("access_"):
            raise RequestSetupError(
                f"Invalid access token: '{access_token}'. An access token must start with 'access_'."
            )
        return access_token

    def __init__(self, api_endpoint=None, timeout=(2, 10), retry=3):
        """Initialize a new Mollie API client.

        :param api_endpoint: The API endpoint to communicate to, this default to the production environment (string)
        :param timeout: The timeouts used for the HTTP requests to the API, the default specifies both connect and
            read timeout (integer or tuple)
        :param retry: The number of retries that the client should perform in case of failed requests. Note that only
            connect errors trigger a retry, errors reponses from the API don't (integer).
        """
        self.api_endpoint = self.validate_api_endpoint(api_endpoint or self.API_ENDPOINT)
        self.api_version = self.API_VERSION
        self.timeout = timeout
        self.retry = retry
        self.api_key = None
        self._client = None

        self._oauth_client = None
        self.client_secret = None
        self.access_token = None
        self.set_token = None

        # add endpoint resources
        self.payments = Payments(self)
        self.payment_links = PaymentLinks(self)
        self.payment_refunds = PaymentRefunds(self)
        self.payment_chargebacks = PaymentChargebacks(self)
        self.profiles = Profiles(self)
        self.profile_chargebacks = ProfileChargebacks(self)
        self.profile_methods = ProfileMethods(self)
        self.profile_payments = ProfilePayments(self)
        self.profile_refunds = ProfileRefunds(self)
        self.methods = Methods(self)
        self.refunds = Refunds(self)
        self.chargebacks = Chargebacks(self)
        self.customers = Customers(self)
        self.customer_mandates = CustomerMandates(self)
        self.customer_subscriptions = CustomerSubscriptions(self)
        self.customer_payments = CustomerPayments(self)
        self.orders = Orders(self)
        self.organizations = Organizations(self)
        self.subscription_payments = SubscriptionPayments(self)
        self.invoices = Invoices(self)
        self.permissions = Permissions(self)
        self.onboarding = Onboarding(self)
        self.captures = Captures(self)

        self.settlements = Settlements(self)