diff --git a/ordway/api/base.py b/ordway/api/base.py index 7f78986..b04472a 100644 --- a/ordway/api/base.py +++ b/ordway/api/base.py @@ -1,6 +1,7 @@ from typing import TYPE_CHECKING, Optional, List, Dict, Any, Generator, Union, Tuple from logging import getLogger from requests.exceptions import RequestException +from ratelimit import limits, sleep_and_retry from ordway.consts import API_ENDPOINT_BASE, STAGING_ENDPOINT_BASE from ordway.utils import transform_datetimes @@ -8,19 +9,75 @@ if TYPE_CHECKING: from ordway.client import OrdwayClient # pylint: disable=cyclic-import + from requests import Session logger = getLogger(__name__) _Response = Union[List[Dict[str, Any]], Dict[str, Any]] +def _request( + session: "Session", + method: str, + endpoint: str, + api_version: str, + params: Optional[Dict[str, str]] = None, + data: Optional[Dict[str, Any]] = None, + json: Optional[Dict[str, Any]] = None, + staging: bool = False, + headers: Dict[str, str] = {}, +): + if staging: + base = STAGING_ENDPOINT_BASE + else: + base = API_ENDPOINT_BASE + + json, data = transform_datetimes(json), transform_datetimes(data) + + url = f"{base}/v{api_version}/{endpoint}" + + logger.debug( + 'Sending a request to Ordway endpoint "%s" with the following query params: %s', + endpoint, + params, + ) + + # Ensure any changes to client attrs are reflected in headers on request. + session.headers.update(headers) + + try: + response = session.request( + method=method, url=url, params=params, data=data, json=json + ) + + response.raise_for_status() + + return response.json() + except RequestException as err: + raise OrdwayAPIRequestException( + str(err), request=err.request, response=err.response + ) from err + except ValueError as err: + raise OrdwayAPIRequestException( + "Ordway returned HTTP success, but no valid JSON was present. Please report this as an issue on GitHub." + ) from err + + class APIBase: collection: str - def __init__(self, client: "OrdwayClient", staging: bool = False): + def __init__(self, client: "OrdwayClient", **kwargs): self.client = client self.session = client.session - self.staging = staging + self.staging = kwargs.get("staging", False) + + self.ratelimit_calls = kwargs.get("calls", 2) + self.ratelimit_period = kwargs.get("period", 1) + + self._ratelimited_request = limits( + calls=self.ratelimit_calls, period=self.ratelimit_period + )(_request) + self._ratelimited_request = sleep_and_retry(self._ratelimited_request) def _construct_headers(self) -> Dict[str, str]: """ Returns a dictionary of headers Ordway always expects for API requests. """ @@ -42,41 +99,18 @@ def _request( # pylint: disable=too-many-arguments data: Optional[Dict[str, Any]] = None, json: Optional[Dict[str, Any]] = None, ) -> _Response: - if self.staging: - base = STAGING_ENDPOINT_BASE - else: - base = API_ENDPOINT_BASE - - json, data = transform_datetimes(json), transform_datetimes(data) - - url = f"{base}/v{self.client.api_version}/{endpoint}" - - logger.debug( - 'Sending a request to Ordway endpoint "%s" with the following query params: %s', - endpoint, - params, + return self._ratelimited_request( + session=self.session, + method=method, + endpoint=endpoint, + params=params, + data=data, + json=json, + api_version=self.client.api_version, + headers=self._construct_headers(), + staging=self.staging, ) - # Ensure any changes to client attrs are reflected in headers on request. - self.session.headers.update(self._construct_headers()) - - try: - response = self.session.request( - method=method, url=url, params=params, data=data, json=json - ) - - response.raise_for_status() - - return response.json() - except RequestException as err: - raise OrdwayAPIRequestException( - str(err), request=err.request, response=err.response - ) from err - except ValueError as err: - raise OrdwayAPIRequestException( - "Ordway returned HTTP success, but no valid JSON was present. Please report this as an issue on GitHub." - ) from err - def _get_request( self, endpoint: str, params: Optional[Dict[str, str]] = None ) -> _Response: diff --git a/ordway/client.py b/ordway/client.py index 0991e61..4f959b1 100644 --- a/ordway/client.py +++ b/ordway/client.py @@ -3,6 +3,7 @@ from os import environ from .session import session_factory +from .utils import to_snake_case from .exceptions import OrdwayClientException from .consts import SUPPORTED_API_VERSIONS from . import api @@ -12,11 +13,35 @@ logger = getLogger(__name__) +DEFAULT_RATELIMITING = {"calls": 2, "period": 1} + class OrdwayClient: # pylint: disable=too-many-instance-attributes """ A client for interacting with Ordway's API (https://ordwaylabs.api-docs.io). """ SUPPORTED_API_VERSIONS = SUPPORTED_API_VERSIONS + INTERFACES = [ + api.Products, + api.Customers, + api.Subscriptions, + api.Invoices, + api.Payments, + api.Credits, + api.Plans, + api.Refunds, + api.Webhooks, + api.JournalEntries, + api.PaymentRuns, + api.Statements, + api.Coupons, + api.Orders, + api.Usages, + api.BillingRuns, + api.RevenueSchedules, + api.BillingSchedules, + api.RevenueRules, + api.ChartOfAccounts, + ] def __init__( # pylint: disable=too-many-arguments self, @@ -25,10 +50,10 @@ def __init__( # pylint: disable=too-many-arguments company: str, user_token: str, api_version: str = "1", - staging: bool = False, proxies: Optional[Dict[str, str]] = None, headers: Optional[Dict[str, str]] = None, session: Optional["Session"] = None, + **kwargs, ): self.email = email self.api_key = api_key @@ -46,27 +71,10 @@ def __init__( # pylint: disable=too-many-arguments self.api_version = api_version - # Interfaces - self.products = api.Products(self, staging=staging) - self.customers = api.Customers(self, staging=staging) - self.subscriptions = api.Subscriptions(self, staging=staging) - self.invoices = api.Invoices(self, staging=staging) - self.payments = api.Payments(self, staging=staging) - self.credits = api.Credits(self, staging=staging) - self.plans = api.Plans(self, staging=staging) - self.refunds = api.Refunds(self, staging=staging) - self.webhooks = api.Webhooks(self, staging=staging) - self.journal_entries = api.JournalEntries(self, staging=staging) - self.payment_runs = api.PaymentRuns(self, staging=staging) - self.statements = api.Statements(self, staging=staging) - self.coupons = api.Coupons(self, staging=staging) - self.orders = api.Orders(self, staging=staging) - self.usages = api.Usages(self, staging=staging) - self.billing_runs = api.BillingRuns(self, staging=staging) - self.revenue_schedules = api.RevenueSchedules(self, staging=staging) - self.billing_schedules = api.BillingSchedules(self, staging=staging) - self.revenue_rules = api.RevenueRules(self, staging=staging) - self.chart_of_accounts = api.ChartOfAccounts(self, staging=staging) + for Interface in self.INTERFACES: + snake_case_name = to_snake_case(Interface.__name__) + + setattr(self, snake_case_name, Interface(self, **kwargs)) @property def api_version(self): diff --git a/ordway/utils.py b/ordway/utils.py index fe272f8..e17a400 100644 --- a/ordway/utils.py +++ b/ordway/utils.py @@ -18,3 +18,15 @@ def transform_datetimes(data: Any) -> Any: data[key] = transform_datetimes(val) return data + + +def to_snake_case(s: str) -> str: + converted = "" + + for i, c in enumerate(s): + if c.isupper() and i != 0: + converted += "_" + + converted += c.lower() + + return converted diff --git a/requirements.txt b/requirements.txt index 4261e68..a517e46 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -requests==2.24.0 \ No newline at end of file +requests==2.24.0 +ratelimit==2.2.1 \ No newline at end of file