Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ratelimiting #10

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
104 changes: 69 additions & 35 deletions ordway/api/base.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,83 @@
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

from .exceptions import OrdwayAPIRequestException, OrdwayAPIException

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. """
Expand All @@ -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:
Expand Down
52 changes: 30 additions & 22 deletions ordway/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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):
Expand Down
12 changes: 12 additions & 0 deletions ordway/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
requests==2.24.0
requests==2.24.0
ratelimit==2.2.1