diff --git a/mollie/api/client.py b/mollie/api/client.py index 255b86c2..45c351a5 100644 --- a/mollie/api/client.py +++ b/mollie/api/client.py @@ -3,6 +3,7 @@ import re import ssl from collections import OrderedDict +from typing import Any, Callable, Dict, List, Optional, Tuple, Union from urllib.parse import urlencode import requests @@ -31,28 +32,41 @@ 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" + CLIENT_VERSION: str = VERSION + API_ENDPOINT: str = "https://api.mollie.com" + API_VERSION: str = "v2" + UNAME: str = " ".join(platform.uname()) + + OAUTH_AUTHORIZATION_URL: str = "https://www.mollie.com/oauth2/authorize" + OAUTH_AUTO_REFRESH_URL: str = API_ENDPOINT + "/oauth2/tokens" + OAUTH_TOKEN_URL: str = API_ENDPOINT + "/oauth2/tokens" + + _client: requests.Session + _oauth_client: OAuth2Session + api_endpoint: str + api_version: str + timeout: Union[int, Tuple[int, int]] + retry: int + api_key: str = "" + access_token: str = "" + user_agent_components: Dict[str, str] + client_id: str = "" + client_secret: str = "" + set_token: Callable[[dict], None] @staticmethod - def validate_api_endpoint(api_endpoint): + def validate_api_endpoint(api_endpoint: str) -> str: return api_endpoint.strip().rstrip("/") @staticmethod - def validate_api_key(api_key): + def validate_api_key(api_key: str) -> str: 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): + def validate_access_token(access_token: str) -> str: access_token = access_token.strip() if not access_token.startswith("access_"): raise RequestSetupError( @@ -60,7 +74,7 @@ def validate_access_token(access_token): ) return access_token - def __init__(self, api_endpoint=None, timeout=(2, 10), retry=3): + def __init__(self, api_endpoint: str = "", timeout: Union[int, Tuple[int, int]] = (2, 10), retry: int = 3) -> None: """Initialize a new Mollie API client. :param api_endpoint: The API endpoint to communicate to, this default to the production environment (string) @@ -73,13 +87,6 @@ def __init__(self, api_endpoint=None, timeout=(2, 10), retry=3): 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) @@ -106,20 +113,20 @@ def __init__(self, api_endpoint=None, timeout=(2, 10), retry=3): "OpenSSL", ssl.OPENSSL_VERSION.split(" ")[1], sanitize=False ) # keep legacy formatting of this component - def set_api_endpoint(self, api_endpoint): + def set_api_endpoint(self, api_endpoint: str) -> None: self.api_endpoint = self.validate_api_endpoint(api_endpoint) - def set_api_key(self, api_key): + def set_api_key(self, api_key: str) -> None: self.api_key = self.validate_api_key(api_key) - def set_access_token(self, access_token): + def set_access_token(self, access_token: str) -> None: self.api_key = self.validate_access_token(access_token) self.set_user_agent_component("OAuth", "2.0", sanitize=False) # keep spelling equal to the PHP client - def set_timeout(self, timeout): + def set_timeout(self, timeout: Union[int, Tuple[int, int]]) -> None: self.timeout = timeout - def set_user_agent_component(self, key, value, sanitize=True): + def set_user_agent_component(self, key: str, value: str, sanitize: bool = True) -> None: """Add or replace new user-agent component strings. Given strings are formatted along the format agreed upon by Mollie and implementers: @@ -137,40 +144,52 @@ def set_user_agent_component(self, key, value, sanitize=True): self.user_agent_components[key] = value @property - def user_agent(self): + def user_agent(self) -> str: """Return the formatted user agent string.""" components = ["/".join(x) for x in self.user_agent_components.items()] return " ".join(components) - def _format_request_data(self, path, data, params): + def _format_request_data( + self, + path: str, + data: Optional[Dict[str, Any]], + params: Optional[Dict[str, Any]], + ) -> Tuple[str, str, Optional[Dict[str, Any]]]: if path.startswith(f"{self.api_endpoint}/{self.api_version}"): url = path else: url = f"{self.api_endpoint}/{self.api_version}/{path}" + payload = "" if data is not None: try: - data = json.dumps(data) - except Exception as err: - raise RequestSetupError(f"Error encoding parameters into JSON: '{err}'.") + payload = json.dumps(data) + except TypeError as err: + raise RequestSetupError(f"Error encoding data into JSON: {err}.") querystring = generate_querystring(params) if querystring: url += "?" + querystring params = None - return url, data, params + return url, payload, params - def _perform_http_call_apikey(self, http_method, path, data=None, params=None): + def _perform_http_call_apikey( + self, + http_method: str, + path: str, + data: Optional[Dict[str, Any]] = None, + params: Optional[Dict[str, Any]] = None, + ) -> requests.Response: if not self.api_key: raise RequestSetupError("You have not set an API key. Please use set_api_key() to set the API key.") - if not self._client: + if not hasattr(self, "_client"): self._client = requests.Session() self._client.verify = True self._setup_retry() - url, data, params = self._format_request_data(path, data, params) + url, payload, params = self._format_request_data(path, data, params) try: response = self._client.request( method=http_method, @@ -183,15 +202,21 @@ def _perform_http_call_apikey(self, http_method, path, data=None, params=None): "X-Mollie-Client-Info": self.UNAME, }, params=params, - data=data, + data=payload, timeout=self.timeout, ) except requests.exceptions.RequestException as err: raise RequestError(f"Unable to communicate with Mollie: {err}") return response - def _perform_http_call_oauth(self, http_method, path, data=None, params=None): - url, data, params = self._format_request_data(path, data, params) + def _perform_http_call_oauth( + self, + http_method: str, + path: str, + data: Optional[Dict[str, Any]] = None, + params: Optional[Dict[str, Any]] = None, + ) -> requests.Response: + url, payload, params = self._format_request_data(path, data, params) try: response = self._oauth_client.request( method=http_method, @@ -203,20 +228,34 @@ def _perform_http_call_oauth(self, http_method, path, data=None, params=None): "X-Mollie-Client-Info": self.UNAME, }, params=params, - data=data, + data=payload, timeout=self.timeout, ) except requests.exceptions.RequestException as err: raise RequestError(f"Unable to communicate with Mollie: {err}") return response - def perform_http_call(self, http_method, path, data=None, params=None): - if self._oauth_client: + def perform_http_call( + self, + http_method: str, + path: str, + data: Optional[Dict[str, Any]] = None, + params: Optional[Dict[str, Any]] = None, + ) -> requests.Response: + if hasattr(self, "_oauth_client"): return self._perform_http_call_oauth(http_method, path, data=data, params=params) else: return self._perform_http_call_apikey(http_method, path, data=data, params=params) - def setup_oauth(self, client_id, client_secret, redirect_uri, scope, token, set_token): + def setup_oauth( + self, + client_id: str, + client_secret: str, + redirect_uri: str, + scope: List[str], + token: str, + set_token: Callable[[dict], None], + ) -> Tuple[bool, Optional[str]]: """ :param client_id: (string) :param client_secret: (string) @@ -251,37 +290,36 @@ def setup_oauth(self, client_id, client_secret, redirect_uri, scope, token, set_ # The merchant should visit this url to authorize access. return self._oauth_client.authorized, authorization_url - def setup_oauth_authorization_response(self, authorization_response): + def setup_oauth_authorization_response(self, authorization_response: str) -> None: """ :param authorization_response: The full callback URL (string) :return: None """ - # Fetch an access token from the provider using the authorization code obtained during user authorization. - self.access_token = self._oauth_client.fetch_token( + # Fetch an OAuth token from the provider using the authorization code obtained during user authorization. + token = self._oauth_client.fetch_token( self.OAUTH_TOKEN_URL, authorization_response=authorization_response, client_secret=self.client_secret, ) - self.set_token(self.access_token) - return self.access_token + self.set_token(token) # TODO Implement https://docs.mollie.com/reference/oauth2/revoke-token # def revoke_oauth_token(self, token, type_hint): # ... - def _setup_retry(self): + def _setup_retry(self) -> None: """Configure a retry behaviour on the HTTP client.""" if self.retry: retry = Retry(connect=self.retry, backoff_factor=1) adapter = requests.adapters.HTTPAdapter(max_retries=retry) - if self._client: + if hasattr(self, "_client"): self._client.mount("https://", adapter) - if self._oauth_client: + elif hasattr(self, "_oauth_client"): self._oauth_client.mount("https://", adapter) -def generate_querystring(params): +def generate_querystring(params: Optional[Dict[str, Any]]) -> Optional[str]: """ Generate a querystring suitable for use in the v2 api. @@ -290,8 +328,10 @@ def generate_querystring(params): """ if not params: return None + parts = [] for param, value in params.items(): + # TODO clean this up with a simple recursive approach if not isinstance(value, dict): parts.append(urlencode({param: value})) else: @@ -299,5 +339,5 @@ def generate_querystring(params): for key, sub_value in value.items(): composed = f"{param}[{key}]" parts.append(urlencode({composed: sub_value})) - if parts: - return "&".join(parts) + + return "&".join(parts) diff --git a/mollie/api/objects/base.py b/mollie/api/objects/base.py index 8eef2aae..0cff42a3 100644 --- a/mollie/api/objects/base.py +++ b/mollie/api/objects/base.py @@ -22,7 +22,7 @@ def _get_link(self, name): except (KeyError, TypeError): return None - def get_embedded(self, name: str): + def get_embedded(self, name: str) -> dict: """ Get embedded data by its name. diff --git a/mollie/api/objects/capture.py b/mollie/api/objects/capture.py index d43ac65f..f77d3332 100644 --- a/mollie/api/objects/capture.py +++ b/mollie/api/objects/capture.py @@ -4,9 +4,9 @@ class Capture(ObjectBase): @classmethod def get_resource_class(cls, client): - from ..resources.captures import Captures + from ..resources import PaymentCaptures - return Captures(client) + return PaymentCaptures(client) @property def id(self): diff --git a/mollie/api/objects/chargeback.py b/mollie/api/objects/chargeback.py index 2e86f05f..0059f323 100644 --- a/mollie/api/objects/chargeback.py +++ b/mollie/api/objects/chargeback.py @@ -6,7 +6,7 @@ class Chargeback(ObjectBase): @classmethod def get_resource_class(cls, client): - from ..resources.chargebacks import Chargebacks + from ..resources import Chargebacks return Chargebacks(client) diff --git a/mollie/api/resources/base.py b/mollie/api/resources/base.py index 8d90a4cd..f62ad39c 100644 --- a/mollie/api/resources/base.py +++ b/mollie/api/resources/base.py @@ -1,8 +1,11 @@ -from typing import Any, Optional +from typing import TYPE_CHECKING, Any, Dict, Optional from ..error import IdentifierError, ResponseError, ResponseHandlingError from ..objects.list import ObjectList +if TYPE_CHECKING: + from ..client import Client + class ResourceBase: DEFAULT_LIMIT = 10 @@ -15,7 +18,7 @@ class ResourceBase: RESOURCE_ID_PREFIX: str = "" - def __init__(self, client): + def __init__(self, client: "Client") -> None: self.client = client def get_resource_object(self, result: dict) -> Any: @@ -32,8 +35,12 @@ def get_resource_path(self) -> str: return self.__class__.__name__.lower() def perform_api_call( - self, http_method: str, path: str, data: Optional[dict] = None, params: Optional[dict] = None - ) -> dict: + self, + http_method: str, + path: str, + data: Optional[Dict[str, Any]] = None, + params: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: resp = self.client.perform_http_call(http_method, path, data, params) if "application/hal+json" in resp.headers.get("Content-Type", ""): # set the content type according to the media type definition @@ -56,7 +63,7 @@ def perform_api_call( return result @classmethod - def validate_resource_id(cls, resource_id: str, name: str = "Identifier", message: Optional[str] = None) -> None: + def validate_resource_id(cls, resource_id: str, name: str = "Identifier", message: str = "") -> None: """Generic identifier validation.""" if not message: message = f"Invalid {name} '{resource_id}', it should start with '{cls.RESOURCE_ID_PREFIX}'." @@ -66,37 +73,39 @@ def validate_resource_id(cls, resource_id: str, name: str = "Identifier", messag class ResourceCreateMixin(ResourceBase): - def create(self, data: Optional[dict] = None, **params): + def create(self, data: Optional[Dict[str, Any]] = None, **params: Optional[Dict[str, Any]]) -> Any: path = self.get_resource_path() result = self.perform_api_call(self.REST_CREATE, path, data, params) return self.get_resource_object(result) class ResourceGetMixin(ResourceBase): - def get(self, resource_id: str, **params): + def get(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> Any: resource_path = self.get_resource_path() path = f"{resource_path}/{resource_id}" result = self.perform_api_call(self.REST_READ, path, params=params) return self.get_resource_object(result) - def from_url(self, url: str, data: Optional[dict] = None, params: Optional[dict] = None): + def from_url(self, url: str, params: Optional[Dict[str, Any]] = None) -> Any: """Utility method to return an object from a full URL (such as from _links). This method always does a GET request and returns a single Object. """ - result = self.perform_api_call(self.REST_READ, url, data, params) + result = self.perform_api_call(self.REST_READ, url, params=params) return self.get_resource_object(result) class ResourceListMixin(ResourceBase): - def list(self, **params): + def list(self, **params: Optional[Dict[str, Any]]) -> ObjectList: path = self.get_resource_path() result = self.perform_api_call(self.REST_LIST, path, params=params) return ObjectList(result, self.get_resource_object({}).__class__, self.client) class ResourceUpdateMixin(ResourceBase): - def update(self, resource_id: str, data: Optional[dict] = None, **params): + def update( + self, resource_id: str, data: Optional[Dict[str, Any]] = None, **params: Optional[Dict[str, Any]] + ) -> Any: resource_path = self.get_resource_path() path = f"{resource_path}/{resource_id}" result = self.perform_api_call(self.REST_UPDATE, path, data, params) @@ -104,7 +113,7 @@ def update(self, resource_id: str, data: Optional[dict] = None, **params): class ResourceDeleteMixin(ResourceBase): - def delete(self, resource_id: str, **params): + def delete(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> Any: resource_path = self.get_resource_path() path = f"{resource_path}/{resource_id}" return self.perform_api_call(self.REST_DELETE, path, params=params) diff --git a/mollie/api/resources/captures.py b/mollie/api/resources/captures.py index 2147ab0a..b86e4e98 100644 --- a/mollie/api/resources/captures.py +++ b/mollie/api/resources/captures.py @@ -1,5 +1,12 @@ +from typing import TYPE_CHECKING, Any, Dict, Optional + from ..objects.capture import Capture -from .base import ResourceGetMixin, ResourceListMixin +from .base import ResourceBase, ResourceGetMixin, ResourceListMixin + +if TYPE_CHECKING: + from ..client import Client + from ..objects.payment import Payment + from ..objects.settlement import Settlement __all__ = [ "PaymentCaptures", @@ -7,26 +14,26 @@ ] -class CapturesBase: - RESOURCE_ID_PREFIX = "cpt_" +class CapturesBase(ResourceBase): + RESOURCE_ID_PREFIX: str = "cpt_" def get_resource_object(self, result: dict) -> Capture: - return Capture(result, self.client) # type: ignore + return Capture(result, self.client) class PaymentCaptures(CapturesBase, ResourceGetMixin, ResourceListMixin): """Resource handler for the `/payments/:payment_id:/captures` endpoint.""" - _payment = None + _payment: "Payment" - def __init__(self, client, payment): + def __init__(self, client: "Client", payment: "Payment") -> None: self._payment = payment super().__init__(client) def get_resource_path(self) -> str: - return f"payments/{self._payment.id}/captures" # type:ignore + return f"payments/{self._payment.id}/captures" - def get(self, resource_id: str, **params): + def get(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> Capture: self.validate_resource_id(resource_id, "capture ID") return super().get(resource_id, **params) @@ -34,11 +41,11 @@ def get(self, resource_id: str, **params): class SettlementCaptures(CapturesBase, ResourceListMixin): """Resource handler for the `/settlements/:settlement_id:/captures` endpoint.""" - _settlement = None + _settlement: "Settlement" - def __init__(self, client, settlement): + def __init__(self, client: "Client", settlement: "Settlement") -> None: self._settlement = settlement super().__init__(client) def get_resource_path(self) -> str: - return f"settlements/{self._settlement.id}/captures" # type:ignore + return f"settlements/{self._settlement.id}/captures" diff --git a/mollie/api/resources/chargebacks.py b/mollie/api/resources/chargebacks.py index 375307d2..44569a53 100644 --- a/mollie/api/resources/chargebacks.py +++ b/mollie/api/resources/chargebacks.py @@ -1,6 +1,15 @@ +from typing import TYPE_CHECKING, Any, Dict, Optional + from ..objects.chargeback import Chargeback +from ..objects.list import ObjectList from .base import ResourceBase, ResourceGetMixin, ResourceListMixin +if TYPE_CHECKING: + from ..client import Client + from ..objects.payment import Payment + from ..objects.profile import Profile + from ..objects.settlement import Settlement + __all__ = [ "Chargebacks", "PaymentChargebacks", @@ -9,11 +18,11 @@ ] -class ChargebacksBase: - RESOURCE_ID_PREFIX = "chb_" +class ChargebacksBase(ResourceBase): + RESOURCE_ID_PREFIX: str = "chb_" def get_resource_object(self, result: dict) -> Chargeback: - return Chargeback(result, self.client) # type: ignore + return Chargeback(result, self.client) class Chargebacks(ChargebacksBase, ResourceListMixin): @@ -25,16 +34,16 @@ class Chargebacks(ChargebacksBase, ResourceListMixin): class PaymentChargebacks(ChargebacksBase, ResourceGetMixin, ResourceListMixin): """Resource handler for the `/payments/:payment_id:/chargebacks` endpoint.""" - _payment = None + _payment: "Payment" - def __init__(self, client, payment): + def __init__(self, client: "Client", payment: "Payment") -> None: self._payment = payment super().__init__(client) def get_resource_path(self) -> str: - return f"payments/{self._payment.id}/chargebacks" # type: ignore + return f"payments/{self._payment.id}/chargebacks" - def get(self, resource_id: str, **params): + def get(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> Chargeback: self.validate_resource_id(resource_id, "chargeback ID") return super().get(resource_id, **params) @@ -42,30 +51,30 @@ def get(self, resource_id: str, **params): class SettlementChargebacks(ChargebacksBase, ResourceListMixin): """Resource handler for the `/settlements/:settlement_id:/chargebacks` endpoint.""" - _settlement = None + _settlement: "Settlement" - def __init__(self, client, settlement): + def __init__(self, client: "Client", settlement: "Settlement") -> None: self._settlement = settlement super().__init__(client) def get_resource_path(self) -> str: - return f"settlements/{self._settlement.id}/chargebacks" # type: ignore + return f"settlements/{self._settlement.id}/chargebacks" -class ProfileChargebacks(ChargebacksBase, ResourceBase): +class ProfileChargebacks(ChargebacksBase): """ Resource handler for the `/chargebacks?profileId=:profile_id:` endpoint. This is separate from the `Chargebacks` resource handler to make it easier to inject the profileId. """ - _profile = None + _profile: "Profile" - def __init__(self, client, profile): + def __init__(self, client: "Client", profile: "Profile") -> None: self._profile = profile super().__init__(client) - def list(self, **params): + def list(self, **params: Optional[Dict[str, Any]]) -> ObjectList: # Set the profileId in the query params params.update({"profileId": self._profile.id}) return Chargebacks(self.client).list(**params) diff --git a/mollie/api/resources/clients.py b/mollie/api/resources/clients.py index 3eb1b985..a98cb559 100644 --- a/mollie/api/resources/clients.py +++ b/mollie/api/resources/clients.py @@ -1,3 +1,5 @@ +from typing import Any, Dict, Optional + from ..objects.client import Client from .base import ResourceGetMixin, ResourceListMixin @@ -13,12 +15,12 @@ class Clients(ResourceListMixin, ResourceGetMixin): Retrieve a list of Mollie merchants connected to your partner account (only for Mollie partners). """ - RESOURCE_ID_PREFIX = "org_" + RESOURCE_ID_PREFIX: str = "org_" def get_resource_object(self, result: dict) -> Client: return Client(result, self.client) - def get(self, resource_id: str, **params): + def get(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> Client: """Retrieve a single client, linked to your partner account, by its ID.""" self.validate_resource_id(resource_id, "client ID") return super().get(resource_id, **params) diff --git a/mollie/api/resources/customers.py b/mollie/api/resources/customers.py index d65aaa94..f9845141 100644 --- a/mollie/api/resources/customers.py +++ b/mollie/api/resources/customers.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Any, Dict, Optional from ..objects.customer import Customer from .base import ResourceCreateMixin, ResourceDeleteMixin, ResourceGetMixin, ResourceListMixin, ResourceUpdateMixin @@ -7,19 +7,21 @@ class Customers(ResourceCreateMixin, ResourceDeleteMixin, ResourceGetMixin, ResourceListMixin, ResourceUpdateMixin): """Resource handler for the `/customers` endpoint.""" - RESOURCE_ID_PREFIX = "cst_" + RESOURCE_ID_PREFIX: str = "cst_" def get_resource_object(self, result: dict) -> Customer: return Customer(result, self.client) - def get(self, resource_id: str, **params): + def get(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> Customer: self.validate_resource_id(resource_id, "customer ID") return super().get(resource_id, **params) - def update(self, resource_id: str, data: Optional[dict] = None, **params): + def update( + self, resource_id: str, data: Optional[Dict[str, Any]] = None, **params: Optional[Dict[str, Any]] + ) -> Customer: self.validate_resource_id(resource_id, "customer ID") return super().update(resource_id, data, **params) - def delete(self, resource_id: str, **params): + def delete(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> dict: self.validate_resource_id(resource_id, "customer ID") return super().delete(resource_id, **params) diff --git a/mollie/api/resources/invoices.py b/mollie/api/resources/invoices.py index 3933dc87..de2f03ef 100644 --- a/mollie/api/resources/invoices.py +++ b/mollie/api/resources/invoices.py @@ -1,3 +1,5 @@ +from typing import Any, Dict, Optional + from ..objects.invoice import Invoice from .base import ResourceGetMixin, ResourceListMixin @@ -9,11 +11,11 @@ class Invoices(ResourceGetMixin, ResourceListMixin): """Resource handler for the `/invoices` endpoint.""" - RESOURCE_ID_PREFIX = "inv_" + RESOURCE_ID_PREFIX: str = "inv_" def get_resource_object(self, result: dict) -> Invoice: return Invoice(result, self.client) - def get(self, resource_id: str, **params): + def get(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> Invoice: self.validate_resource_id(resource_id, "invoice ID") return super().get(resource_id, **params) diff --git a/mollie/api/resources/mandates.py b/mollie/api/resources/mandates.py index e74e3c2e..5453ba9a 100644 --- a/mollie/api/resources/mandates.py +++ b/mollie/api/resources/mandates.py @@ -1,6 +1,13 @@ +from typing import TYPE_CHECKING, Any, Dict, Optional + +from ..objects.customer import Customer from ..objects.mandate import Mandate from .base import ResourceCreateMixin, ResourceDeleteMixin, ResourceGetMixin, ResourceListMixin +if TYPE_CHECKING: + from ..client import Client + + __all__ = [ "CustomerMandates", ] @@ -11,22 +18,22 @@ class CustomerMandates(ResourceCreateMixin, ResourceDeleteMixin, ResourceGetMixi RESOURCE_ID_PREFIX = "mdt_" - _customer = None + _customer: Customer - def __init__(self, client, customer): + def __init__(self, client: "Client", customer: Customer) -> None: self._customer = customer super().__init__(client) def get_resource_path(self) -> str: - return f"customers/{self._customer.id}/mandates" # type: ignore + return f"customers/{self._customer.id}/mandates" def get_resource_object(self, result: dict) -> Mandate: return Mandate(result, self.client) - def get(self, resource_id: str, **params): + def get(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> Mandate: self.validate_resource_id(resource_id, "mandate ID") return super().get(resource_id, **params) - def delete(self, resource_id: str, **params): + def delete(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> dict: self.validate_resource_id(resource_id, "mandate ID") return super().delete(resource_id, **params) diff --git a/mollie/api/resources/methods.py b/mollie/api/resources/methods.py index 4544c1ea..1696ffb6 100644 --- a/mollie/api/resources/methods.py +++ b/mollie/api/resources/methods.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional from ..error import IdentifierError from ..objects.issuer import Issuer @@ -6,21 +6,26 @@ from ..objects.method import Method from .base import ResourceBase, ResourceGetMixin, ResourceListMixin +if TYPE_CHECKING: + from ..client import Client + from ..objects.profile import Profile + + __all__ = [ "Methods", "ProfileMethods", ] -class MethodsBase: +class MethodsBase(ResourceBase): def get_resource_object(self, result: dict) -> Method: - return Method(result, self.client) # type: ignore + return Method(result, self.client) class Methods(MethodsBase, ResourceGetMixin, ResourceListMixin): """Resource handler for the `/methods` endpoint.""" - def all(self, **params) -> ObjectList: + def all(self, **params: Optional[Dict[str, Any]]) -> ObjectList: """List all mollie payment methods, including methods that aren't activated in your profile.""" resource_path = self.get_resource_path() path = f"{resource_path}/all" @@ -28,27 +33,27 @@ def all(self, **params) -> ObjectList: return ObjectList(result, Method, self.client) -class ProfileMethods(MethodsBase, ResourceBase): +class ProfileMethods(MethodsBase): """Resource handler for the `/profiles/:profile_id:/methods` endpoint.""" - _profile = None + _profile: "Profile" - def __init__(self, client, profile): + def __init__(self, client: "Client", profile: "Profile") -> None: self._profile = profile super().__init__(client) def get_resource_path(self) -> str: - return f"profiles/{self._profile.id}/methods" # type: ignore + return f"profiles/{self._profile.id}/methods" @property - def payment_method_requires_issuer(self): + def payment_method_requires_issuer(self) -> List[str]: """A list of payment methods that requires management of specific issuers.""" return [ Method.GIFTCARD, Method.VOUCHER, ] - def enable(self, method_id: str, **params): + def enable(self, method_id: str, **params: Optional[Dict[str, Any]]) -> Method: """ Enable payment method for profile. @@ -65,7 +70,7 @@ def enable(self, method_id: str, **params): result = self.perform_api_call(self.REST_CREATE, path, params=params) return self.get_resource_object(result) - def disable(self, method_id: str, **params): + def disable(self, method_id: str, **params: Optional[Dict[str, Any]]) -> Method: """ Disable payment method for the profile. @@ -82,13 +87,15 @@ def disable(self, method_id: str, **params): result = self.perform_api_call(self.REST_DELETE, path, params=params) return self.get_resource_object(result) - def list(self, **params): + def list(self, **params: Optional[Dict[str, Any]]) -> ObjectList: """List the payment methods for the profile.""" params.update({"profileId": self._profile.id}) # Divert the API call to the general Methods resource return Methods(self.client).list(**params) - def enable_issuer(self, method_id: str, issuer_id: str, data: Optional[dict] = None, **params): + def enable_issuer( + self, method_id: str, issuer_id: str, data: Optional[Dict[str, Any]] = None, **params: Optional[Dict[str, Any]] + ) -> Issuer: """ Enable an issuer for a payment method. @@ -108,7 +115,7 @@ def enable_issuer(self, method_id: str, issuer_id: str, data: Optional[dict] = N result = self.perform_api_call(self.REST_CREATE, path, data, params) return Issuer(result, self.client) - def disable_issuer(self, method_id: str, issuer_id: str, **params): + def disable_issuer(self, method_id: str, issuer_id: str, **params: Optional[Dict[str, Any]]) -> Issuer: """ Disable an issuer for a payment method. diff --git a/mollie/api/resources/onboarding.py b/mollie/api/resources/onboarding.py index b5d6273c..629b1c6d 100644 --- a/mollie/api/resources/onboarding.py +++ b/mollie/api/resources/onboarding.py @@ -1,3 +1,5 @@ +from typing import Any, Dict, Optional + from ..error import IdentifierError from ..objects.onboarding import Onboarding as OnboardingObject from .base import ResourceGetMixin @@ -13,12 +15,12 @@ class Onboarding(ResourceGetMixin): def get_resource_object(self, result: dict) -> OnboardingObject: return OnboardingObject(result, self.client) - def get(self, resource_id: str, **params): + def get(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> OnboardingObject: if resource_id != "me": raise IdentifierError(f"Invalid onboarding ID: '{resource_id}'. The onboarding ID should be 'me'.") return super().get(resource_id, **params) - def create(self, data: dict, **params): + def create(self, data: Dict[str, Any], **params: Optional[Dict[str, Any]]) -> OnboardingObject: resource_path = self.get_resource_path() path = f"{resource_path}/me" result = self.perform_api_call(self.REST_CREATE, path, data, params) diff --git a/mollie/api/resources/order_lines.py b/mollie/api/resources/order_lines.py index 0b257292..a16694c9 100644 --- a/mollie/api/resources/order_lines.py +++ b/mollie/api/resources/order_lines.py @@ -1,10 +1,14 @@ -from typing import Optional +from typing import TYPE_CHECKING, Any, Dict, Optional from ..error import DataConsistencyError from ..objects.list import ObjectList from ..objects.order_line import OrderLine from .base import ResourceBase +if TYPE_CHECKING: + from ..client import Client + from ..objects.order import Order + __all__ = [ "OrderLines", ] @@ -20,21 +24,21 @@ class OrderLines(ResourceBase): implementations in this class. """ - RESOURCE_ID_PREFIX = "odl_" + RESOURCE_ID_PREFIX: str = "odl_" - _order = None + _order: "Order" - def __init__(self, client, order): + def __init__(self, client: "Client", order: "Order") -> None: self._order = order super().__init__(client) def get_resource_path(self) -> str: - return f"orders/{self._order.id}/lines" # type: ignore + return f"orders/{self._order.id}/lines" def get_resource_object(self, result: dict) -> OrderLine: return OrderLine(result, self.client) - def delete_lines(self, data: Optional[dict] = None, **params): + def delete_lines(self, data: Optional[Dict[str, Any]] = None, **params: Optional[Dict[str, Any]]) -> dict: """ Cancel multiple orderlines. @@ -65,7 +69,7 @@ def delete_lines(self, data: Optional[dict] = None, **params): path = self.get_resource_path() return self.perform_api_call(self.REST_DELETE, path, data=data, params=params) - def delete(self, order_line_id: str, **params): + def delete(self, order_line_id: str, **params: Optional[Dict[str, Any]]) -> dict: """ Cancel a single orderline. @@ -79,7 +83,9 @@ def delete(self, order_line_id: str, **params): } return self.delete_lines(data, **params) - def update(self, order_line_id: str, data: Optional[dict] = None, **params): + def update( + self, order_line_id: str, data: Optional[Dict[str, Any]] = None, **params: Optional[Dict[str, Any]] + ) -> OrderLine: """ Custom handling for updating orderlines. @@ -98,7 +104,7 @@ def update(self, order_line_id: str, data: Optional[dict] = None, **params): raise DataConsistencyError(f"OrderLine with id '{order_line_id}' not found in response.") - def list(self, **params): + def list(self, **params: Optional[Dict[str, Any]]) -> ObjectList: """Return the orderline data from the related order.""" lines = self._order._get_property("lines") or [] data = { diff --git a/mollie/api/resources/orders.py b/mollie/api/resources/orders.py index 6ef2cee1..ba5a2a35 100644 --- a/mollie/api/resources/orders.py +++ b/mollie/api/resources/orders.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Any, Dict, Optional from ..objects.order import Order from .base import ResourceCreateMixin, ResourceDeleteMixin, ResourceGetMixin, ResourceListMixin, ResourceUpdateMixin @@ -11,16 +11,16 @@ class Orders(ResourceCreateMixin, ResourceDeleteMixin, ResourceGetMixin, ResourceListMixin, ResourceUpdateMixin): """Resource handler for the `/orders` endpoint.""" - RESOURCE_ID_PREFIX = "ord_" + RESOURCE_ID_PREFIX: str = "ord_" def get_resource_object(self, result: dict) -> Order: return Order(result, self.client) - def get(self, resource_id: str, **params): + def get(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> Order: self.validate_resource_id(resource_id, "order ID") return super().get(resource_id, **params) - def delete(self, resource_id: str, **params): + def delete(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> dict: """Cancel order and return the order object. Deleting an order causes the order status to change to canceled. @@ -30,7 +30,9 @@ def delete(self, resource_id: str, **params): result = super().delete(resource_id, **params) return self.get_resource_object(result) - def update(self, resource_id: str, data: Optional[dict] = None, **params): + def update( + self, resource_id: str, data: Optional[Dict[str, Any]] = None, **params: Optional[Dict[str, Any]] + ) -> Order: """Update an order, and return the updated order.""" self.validate_resource_id(resource_id, "order ID") return super().update(resource_id, data, **params) diff --git a/mollie/api/resources/organizations.py b/mollie/api/resources/organizations.py index adf34e08..c8eb2259 100644 --- a/mollie/api/resources/organizations.py +++ b/mollie/api/resources/organizations.py @@ -1,3 +1,5 @@ +from typing import Any, Dict, Optional + from ..objects.organization import Organization from .base import ResourceGetMixin @@ -9,12 +11,12 @@ class Organizations(ResourceGetMixin): """Resource handler for the `/organizations` endpoint.""" - RESOURCE_ID_PREFIX = "org_" + RESOURCE_ID_PREFIX: str = "org_" def get_resource_object(self, result: dict) -> Organization: return Organization(result, self.client) - def get(self, resource_id: str, **params): + def get(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> Organization: if resource_id != "me": self.validate_resource_id(resource_id, "organization ID") return super().get(resource_id, **params) diff --git a/mollie/api/resources/payment_links.py b/mollie/api/resources/payment_links.py index b5c1cdea..54bfc9f9 100644 --- a/mollie/api/resources/payment_links.py +++ b/mollie/api/resources/payment_links.py @@ -1,3 +1,5 @@ +from typing import Any, Dict, Optional + from ..objects.payment_link import PaymentLink from .base import ResourceCreateMixin, ResourceGetMixin, ResourceListMixin @@ -9,14 +11,14 @@ class PaymentLinks(ResourceCreateMixin, ResourceGetMixin, ResourceListMixin): """Resource handler for the `/payment_links` endpoint.""" - RESOURCE_ID_PREFIX = "pl_" + RESOURCE_ID_PREFIX: str = "pl_" def get_resource_path(self) -> str: return "payment-links" - def get_resource_object(self, result) -> PaymentLink: - return PaymentLink(result, self.client) # type: ignore + def get_resource_object(self, result: dict) -> PaymentLink: + return PaymentLink(result, self.client) - def get(self, resource_id: str, **params): + def get(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> PaymentLink: self.validate_resource_id(resource_id, "payment link ID") return super().get(resource_id, **params) diff --git a/mollie/api/resources/payments.py b/mollie/api/resources/payments.py index de45eaf7..d66221b6 100644 --- a/mollie/api/resources/payments.py +++ b/mollie/api/resources/payments.py @@ -1,7 +1,11 @@ -from typing import Optional +from typing import TYPE_CHECKING, Any, Dict, Optional +from ..objects.customer import Customer from ..objects.list import ObjectList +from ..objects.order import Order from ..objects.payment import Payment +from ..objects.profile import Profile +from ..objects.subscription import Subscription from .base import ( ResourceBase, ResourceCreateMixin, @@ -11,6 +15,10 @@ ResourceUpdateMixin, ) +if TYPE_CHECKING: + from ..client import Client + from ..objects.settlement import Settlement + __all__ = [ "CustomerPayments", "OrderPayments", @@ -21,13 +29,13 @@ ] -class PaymentsBase: - RESOURCE_ID_PREFIX = "tr_" +class PaymentsBase(ResourceBase): + RESOURCE_ID_PREFIX: str = "tr_" def get_resource_object(self, result: dict) -> Payment: from ..objects.payment import Payment - return Payment(result, self.client) # type: ignore + return Payment(result, self.client) class Payments( @@ -35,11 +43,11 @@ class Payments( ): """Resource handler for the `/payments` endpoint.""" - def get(self, resource_id: str, **params): + def get(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> Payment: self.validate_resource_id(resource_id, "payment ID") return super().get(resource_id, **params) - def delete(self, resource_id: str, **params): + def delete(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> dict: """Cancel payment and return the payment object. Deleting a payment causes the payment status to change to canceled. @@ -49,7 +57,9 @@ def delete(self, resource_id: str, **params): result = super().delete(resource_id, **params) return self.get_resource_object(result) - def update(self, resource_id: str, data: Optional[dict] = None, **params): + def update( + self, resource_id: str, data: Optional[Dict[str, Any]] = None, **params: Optional[Dict[str, Any]] + ) -> Payment: self.validate_resource_id(resource_id, "payment ID") return super().update(resource_id, data, **params) @@ -57,14 +67,14 @@ def update(self, resource_id: str, data: Optional[dict] = None, **params): class OrderPayments(PaymentsBase, ResourceCreateMixin): """Resource handler for the `/orders/:order_id:/payments` endpoint.""" - _order = None + _order: Order - def __init__(self, client, order): + def __init__(self, client: "Client", order: Order) -> None: self._order = order super().__init__(client) def get_resource_path(self) -> str: - return f"orders/{self._order.id}/payments" # type: ignore + return f"orders/{self._order.id}/payments" def list(self) -> ObjectList: """ @@ -72,7 +82,7 @@ def list(self) -> ObjectList: When you receive an EmbedError, you need to embed the payments in the parent order. """ - payments = self._order.get_embedded("payments") # type: ignore + payments = self._order.get_embedded("payments") data = { "_embedded": { @@ -86,58 +96,58 @@ def list(self) -> ObjectList: class CustomerPayments(PaymentsBase, ResourceCreateMixin, ResourceListMixin): """Resource handler for the `/customers/:customer_id:/payments` endpoint.""" - _customer = None + _customer: Customer - def __init__(self, client, customer): + def __init__(self, client: "Client", customer: Customer) -> None: self._customer = customer super().__init__(client) def get_resource_path(self) -> str: - return f"customers/{self._customer.id}/payments" # type: ignore + return f"customers/{self._customer.id}/payments" class SubscriptionPayments(PaymentsBase, ResourceListMixin): """Resource handler for the `/customers/:customer_id:/subscriptions/:subscription_id:/payments` endpoint.""" - _customer = None - _subscription = None + _customer: Customer + _subscription: Subscription - def __init__(self, client, customer, subscription): + def __init__(self, client: "Client", customer: Customer, subscription: Subscription) -> None: self._customer = customer self._subscription = subscription super().__init__(client) def get_resource_path(self) -> str: - return f"customers/{self._customer.id}/subscriptions/{self._subscription.id}/payments" # type: ignore + return f"customers/{self._customer.id}/subscriptions/{self._subscription.id}/payments" class SettlementPayments(PaymentsBase, ResourceListMixin): """Resource handler for the `/settlements/:settlement_id:/payments` endpoint.""" - _settlement = None + _settlement: "Settlement" - def __init__(self, client, settlement): + def __init__(self, client: "Client", settlement: "Settlement") -> None: self._settlement = settlement super().__init__(client) def get_resource_path(self) -> str: - return f"settlements/{self._settlement.id}/payments" # type: ignore + return f"settlements/{self._settlement.id}/payments" -class ProfilePayments(PaymentsBase, ResourceBase): +class ProfilePayments(PaymentsBase): """ Resource handler for the `/payments?profileId=:profile_id:` endpoint. This is separate from the `Payments` resource handler to make it easier to inject the profileId. """ - _profile = None + _profile: Profile - def __init__(self, client, profile): + def __init__(self, client: "Client", profile: Profile) -> None: self._profile = profile super().__init__(client) - def list(self, **params): + def list(self, **params: Optional[Dict[str, Any]]) -> ObjectList: # Set the profileId in the query params params.update({"profileId": self._profile.id}) return Payments(self.client).list(**params) diff --git a/mollie/api/resources/permissions.py b/mollie/api/resources/permissions.py index 6476ad83..15a2a5ee 100644 --- a/mollie/api/resources/permissions.py +++ b/mollie/api/resources/permissions.py @@ -1,4 +1,5 @@ import re +from typing import Any, Dict, Optional from ..error import IdentifierError from ..objects.permission import Permission @@ -16,10 +17,10 @@ def get_resource_object(self, result: dict) -> Permission: return Permission(result, self.client) @staticmethod - def validate_permission_id(permission_id: str): + def validate_permission_id(permission_id: str) -> None: 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, resource_id: str, **params): + def get(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> Permission: self.validate_resource_id(resource_id) return super().get(resource_id, **params) diff --git a/mollie/api/resources/profiles.py b/mollie/api/resources/profiles.py index e3c33242..14e4c84a 100644 --- a/mollie/api/resources/profiles.py +++ b/mollie/api/resources/profiles.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Any, Dict, Optional from ..objects.profile import Profile from .base import ResourceCreateMixin, ResourceDeleteMixin, ResourceGetMixin, ResourceListMixin, ResourceUpdateMixin @@ -11,20 +11,22 @@ class Profiles(ResourceCreateMixin, ResourceDeleteMixin, ResourceGetMixin, ResourceListMixin, ResourceUpdateMixin): """Resource handler for the `/profiles` endpoint.""" - RESOURCE_ID_PREFIX = "pfl_" + RESOURCE_ID_PREFIX: str = "pfl_" def get_resource_object(self, result: dict) -> Profile: return Profile(result, self.client) - def get(self, resource_id: str, **params): + def get(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> Profile: if resource_id != "me": self.validate_resource_id(resource_id, "profile ID") return super().get(resource_id, **params) - def delete(self, resource_id: str, **params): + def delete(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> dict: self.validate_resource_id(resource_id, "profile ID") return super().delete(resource_id, **params) - def update(self, resource_id: str, data: Optional[dict] = None, **params): + def update( + self, resource_id: str, data: Optional[Dict[str, Any]] = None, **params: Optional[Dict[str, Any]] + ) -> Profile: self.validate_resource_id(resource_id, "profile ID") return super().update(resource_id, **params) diff --git a/mollie/api/resources/refunds.py b/mollie/api/resources/refunds.py index 16798fb0..553e1459 100644 --- a/mollie/api/resources/refunds.py +++ b/mollie/api/resources/refunds.py @@ -1,8 +1,16 @@ -from typing import Optional +from typing import TYPE_CHECKING, Any, Dict, Optional +from ..objects.list import ObjectList +from ..objects.order import Order +from ..objects.payment import Payment +from ..objects.profile import Profile from ..objects.refund import Refund from .base import ResourceBase, ResourceCreateMixin, ResourceDeleteMixin, ResourceGetMixin, ResourceListMixin +if TYPE_CHECKING: + from ..client import Client + from ..objects.settlement import Settlement + __all__ = [ "OrderRefunds", "PaymentRefunds", @@ -12,11 +20,11 @@ ] -class RefundsBase: - RESOURCE_ID_PREFIX = "re_" +class RefundsBase(ResourceBase): + RESOURCE_ID_PREFIX: str = "re_" def get_resource_object(self, result: dict) -> Refund: - return Refund(result, self.client) # type:ignore + return Refund(result, self.client) class Refunds(RefundsBase, ResourceListMixin): @@ -28,20 +36,20 @@ class Refunds(RefundsBase, ResourceListMixin): class PaymentRefunds(RefundsBase, ResourceCreateMixin, ResourceDeleteMixin, ResourceGetMixin, ResourceListMixin): """Resource handler for the `/payments/:payment_id:/refunds` endpoint.""" - _payment = None + _payment: Payment - def __init__(self, client, payment): + def __init__(self, client: "Client", payment: Payment) -> None: self._payment = payment super().__init__(client) def get_resource_path(self) -> str: - return f"payments/{self._payment.id}/refunds" # type: ignore + return f"payments/{self._payment.id}/refunds" - def get(self, resource_id: str, **params): + def get(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> Refund: self.validate_resource_id(resource_id, "Refund ID") return super().get(resource_id, **params) - def delete(self, resource_id: str, **params): + def delete(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> dict: self.validate_resource_id(resource_id, "Refund ID") return super().delete(resource_id, **params) @@ -49,16 +57,16 @@ def delete(self, resource_id: str, **params): class OrderRefunds(RefundsBase, ResourceCreateMixin, ResourceListMixin): """Resource handler for the `/orders/:order_id:/refunds` endpoint.""" - _order = None + _order: Order - def __init__(self, client, order): + def __init__(self, client: "Client", order: Order) -> None: super().__init__(client) self._order = order def get_resource_path(self) -> str: - return f"orders/{self._order.id}/refunds" # type: ignore + return f"orders/{self._order.id}/refunds" - def create(self, data: Optional[dict] = None, **params): + def create(self, data: Optional[Dict[str, Any]] = None, **params: Optional[Dict[str, Any]]) -> Refund: """Create a refund for the order. When no data arg is given, a refund for all order lines is assumed.""" if not data: data = {"lines": []} @@ -68,30 +76,30 @@ def create(self, data: Optional[dict] = None, **params): class SettlementRefunds(RefundsBase, ResourceListMixin): """ResourceHandler for the `/settlements/:settlement_id:/refunds` endpoint.""" - _settlement = None + _settlement: "Settlement" - def __init__(self, client, settlement): + def __init__(self, client: "Client", settlement: "Settlement"): super().__init__(client) self._settlement = settlement def get_resource_path(self) -> str: - return f"settlements/{self._settlement.id}/refunds" # type: ignore + return f"settlements/{self._settlement.id}/refunds" -class ProfileRefunds(RefundsBase, ResourceBase): +class ProfileRefunds(RefundsBase): """ Resource handler for the `/refunds?profileId=:profile_id:` endpoint. This is separate from the `Refunds` resource handler to make it easier to inject the profileId. """ - _profile = None + _profile: Profile - def __init__(self, client, profile): + def __init__(self, client: "Client", profile: Profile) -> None: self._profile = profile super().__init__(client) - def list(self, **params) -> Refunds: + def list(self, **params: Optional[Dict[str, Any]]) -> ObjectList: # Set the profileId in the query params - params.update({"profileId": self._profile.id}) # type: ignore + params.update({"profileId": self._profile.id}) return Refunds(self.client).list(**params) diff --git a/mollie/api/resources/settlements.py b/mollie/api/resources/settlements.py index f34c06b9..5187a051 100644 --- a/mollie/api/resources/settlements.py +++ b/mollie/api/resources/settlements.py @@ -1,5 +1,5 @@ import re -from typing import Optional +from typing import Any, Dict, Optional, Pattern from ..error import IdentifierError from ..objects.settlement import Settlement @@ -9,20 +9,20 @@ class Settlements(ResourceGetMixin, ResourceListMixin): """Resource handler for the `/settlements` endpoint.""" - RESOURCE_ID_PREFIX = "stl_" + RESOURCE_ID_PREFIX: str = "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) + BANK_REFERENCE_REGEX: Pattern[str] = re.compile(r"^\d{4,7}\.\d{4}\.\d{2}$", re.ASCII) def get_resource_object(self, result: dict) -> Settlement: - return Settlement(result, self.client) # type: ignore + return Settlement(result, self.client) @classmethod - def validate_resource_id(cls, resource_id: str, name: str = "", message: Optional[str] = None) -> None: + def validate_resource_id(cls, resource_id: str, name: str = "", message: str = "") -> None: """ Validate the reference id to a settlement. @@ -49,6 +49,6 @@ def validate_resource_id(cls, resource_id: str, name: str = "", message: Optiona except IdentifierError: raise IdentifierError(exc_message) - def get(self, resource_id: str, **params): + def get(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> Settlement: self.validate_resource_id(resource_id) return super().get(resource_id, **params) diff --git a/mollie/api/resources/shipments.py b/mollie/api/resources/shipments.py index bd4dc244..f0cfef75 100644 --- a/mollie/api/resources/shipments.py +++ b/mollie/api/resources/shipments.py @@ -1,8 +1,12 @@ -from typing import Optional +from typing import TYPE_CHECKING, Any, Dict, Optional +from ..objects.order import Order from ..objects.shipment import Shipment from .base import ResourceCreateMixin, ResourceGetMixin, ResourceListMixin, ResourceUpdateMixin +if TYPE_CHECKING: + from ..client import Client + __all__ = [ "OrderShipments", ] @@ -11,21 +15,21 @@ class OrderShipments(ResourceCreateMixin, ResourceGetMixin, ResourceListMixin, ResourceUpdateMixin): """Resource handler for the `/orders/:order_id:/shipments` endpoint.""" - RESOURCE_ID_PREFIX = "shp_" + RESOURCE_ID_PREFIX: str = "shp_" - _order = None + _order: Order - def __init__(self, client, order): + def __init__(self, client: "Client", order: Order) -> None: self._order = order super().__init__(client) def get_resource_object(self, result: dict) -> Shipment: - return Shipment(result, self.client) # type: ignore + return Shipment(result, self.client) def get_resource_path(self) -> str: - return f"orders/{self._order.id}/shipments" # type: ignore + return f"orders/{self._order.id}/shipments" - def create(self, data: Optional[dict] = None, **params): + def create(self, data: Optional[Dict[str, Any]] = None, **params: Optional[Dict[str, Any]]) -> Shipment: """Create a shipment for an order. If the data parameter is omitted, a shipment for all order lines is assumed. @@ -34,10 +38,12 @@ def create(self, data: Optional[dict] = None, **params): data = {"lines": []} return super().create(data, **params) - def get(self, resource_id: str, **params): + def get(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> Shipment: self.validate_resource_id(resource_id, "shipment ID") return super().get(resource_id, **params) - def update(self, resource_id: str, data: Optional[dict] = None, **params): + def update( + self, resource_id: str, data: Optional[Dict[str, Any]] = None, **params: Optional[Dict[str, Any]] + ) -> Shipment: self.validate_resource_id(resource_id, "shipment ID") return super().update(resource_id, data, **params) diff --git a/mollie/api/resources/subscriptions.py b/mollie/api/resources/subscriptions.py index d920a42f..fd50a270 100644 --- a/mollie/api/resources/subscriptions.py +++ b/mollie/api/resources/subscriptions.py @@ -1,7 +1,18 @@ -from typing import Optional +from typing import TYPE_CHECKING, Any, Dict, Optional +from ..objects.customer import Customer from ..objects.subscription import Subscription -from .base import ResourceCreateMixin, ResourceDeleteMixin, ResourceGetMixin, ResourceListMixin, ResourceUpdateMixin +from .base import ( + ResourceBase, + ResourceCreateMixin, + ResourceDeleteMixin, + ResourceGetMixin, + ResourceListMixin, + ResourceUpdateMixin, +) + +if TYPE_CHECKING: + from ..client import Client __all__ = [ "CustomerSubscriptions", @@ -9,11 +20,11 @@ ] -class SubscriptionsBase: - RESOURCE_ID_PREFIX = "sub_" +class SubscriptionsBase(ResourceBase): + RESOURCE_ID_PREFIX: str = "sub_" def get_resource_object(self, result: dict) -> Subscription: - return Subscription(result, self.client) # type: ignore + return Subscription(result, self.client) class Subscriptions(SubscriptionsBase, ResourceListMixin): @@ -32,24 +43,26 @@ class CustomerSubscriptions( ): """Resource handler for the `/customers/:customer_id:/subscriptions` endpoint.""" - _customer = None + _customer: Customer - def __init__(self, client, customer): + def __init__(self, client: "Client", customer: Customer) -> None: self._customer = customer super().__init__(client) def get_resource_path(self) -> str: - return f"customers/{self._customer.id}/subscriptions" # type:ignore + return f"customers/{self._customer.id}/subscriptions" - def get(self, resource_id: str, **params): + def get(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> Subscription: self.validate_resource_id(resource_id, "subscription ID") return super().get(resource_id, **params) - def update(self, resource_id: str, data: Optional[dict] = None, **params): + def update( + self, resource_id: str, data: Optional[Dict[str, Any]] = None, **params: Optional[Dict[str, Any]] + ) -> Subscription: self.validate_resource_id(resource_id, "subscription ID") return super().update(resource_id, data, **params) - def delete(self, resource_id: str, **params): + def delete(self, resource_id: str, **params: Optional[Dict[str, Any]]) -> dict: self.validate_resource_id(resource_id, "subscription ID") resp = super().delete(resource_id, **params) return self.get_resource_object(resp) diff --git a/mypy.ini b/mypy.ini index 44430aaa..745879a7 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,4 +1,11 @@ [mypy] +warn_unused_configs = True +warn_redundant_casts = True +warn_unused_ignores = True +no_implicit_optional = True +strict_equality = True +strict_concatenate = True +disallow_incomplete_defs = True [mypy-requests_oauthlib.*] # requests-oauthlib-1.3.1 has no types yet, but: https://github.com/requests/requests-oauthlib/issues/428 diff --git a/pyproject.toml b/pyproject.toml index 6ef763fd..85d87aff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,3 +16,9 @@ addopts = """ --cov-report=term-missing --cov-branch """ + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "if TYPE_CHECKING:", +] diff --git a/tests/test_api_client.py b/tests/test_api_client.py index 8049da53..69313bc7 100644 --- a/tests/test_api_client.py +++ b/tests/test_api_client.py @@ -127,15 +127,29 @@ def test_client_generic_request_error(response, oauth_client): def test_client_invalid_create_data(client): """Invalid data for a create command should raise an error.""" data = datetime.now() - with pytest.raises(RequestSetupError, match="Error encoding parameters into JSON"): + with pytest.raises(RequestSetupError) as excinfo: client.customers.create(data=data) + assert str(excinfo.value) == "Error encoding data into JSON: Object of type datetime is not JSON serializable." + + # Also test with nested data + data = {"date": datetime.now()} + with pytest.raises(RequestSetupError) as excinfo: + client.customers.create(data=data) + assert str(excinfo.value) == "Error encoding data into JSON: Object of type datetime is not JSON serializable." def test_client_invalid_update_data(client): """Invalid data for a create command should raise an error.""" data = datetime.now() - with pytest.raises(RequestSetupError, match="Error encoding parameters into JSON"): + with pytest.raises(RequestSetupError) as excinfo: + client.customers.update("cst_12345", data=data) + assert str(excinfo.value) == "Error encoding data into JSON: Object of type datetime is not JSON serializable." + + # Also test with nested data + data = {"date": datetime.now()} + with pytest.raises(RequestSetupError) as excinfo: client.customers.update("cst_12345", data=data) + assert str(excinfo.value) == "Error encoding data into JSON: Object of type datetime is not JSON serializable." def test_client_invalid_json_response(client, response):