Skip to content

Commit

Permalink
Abstracted client
Browse files Browse the repository at this point in the history
  • Loading branch information
ChemicalLuck committed Jul 17, 2024
1 parent f3b334b commit c3e9af8
Show file tree
Hide file tree
Showing 40 changed files with 710 additions and 639 deletions.
39 changes: 10 additions & 29 deletions recharge/__init__.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
import logging
from typing import Optional

from requests import Session

import recharge.api.v1 as v1
import recharge.api.v2 as v2
from recharge.api import RechargeScope
from recharge.client import RechargeClient


class RechargeAPIv1Helper:
def __init__(
self,
session: Session,
logger: Optional[logging.Logger] = None,
client: RechargeClient,
scopes: list[RechargeScope] = [],
):
kwargs = {
"session": session,
"logger": logger or logging.getLogger(__name__),
"client": client,
"scopes": scopes,
}

Expand All @@ -41,13 +38,11 @@ def __init__(
class RechargeAPIv2Helper:
def __init__(
self,
session: Session,
logger: Optional[logging.Logger] = None,
client: RechargeClient,
scopes: list[RechargeScope] = [],
):
kwargs = {
"session": session,
"logger": logger or logging.getLogger(__name__),
"client": client,
"scopes": scopes,
}

Expand Down Expand Up @@ -76,29 +71,15 @@ def __init__(

class RechargeAPI(object):
def __init__(self, access_token: str, logger: Optional[logging.Logger] = None):
self.headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"X-Recharge-Access-Token": access_token,
}
self.session = Session()
self.session.headers.update(self.headers)

kwargs = {
"session": self.session,
"logger": logger or logging.getLogger(__name__),
}
self.client = RechargeClient(access_token, logger=logger)

from recharge.api.v1 import TokenResource

self.Token = TokenResource(**kwargs)

self.scopes = self.Token.get()["scopes"]

kwargs["scopes"] = self.scopes
token = TokenResource(self.client)
self.scopes = token.get()["scopes"]

self.v1 = RechargeAPIv1Helper(**kwargs)
self.v2 = RechargeAPIv2Helper(**kwargs)
self.v1 = RechargeAPIv1Helper(self.client, self.scopes)
self.v2 = RechargeAPIv2Helper(self.client, self.scopes)


__all__ = ["RechargeAPI"]
263 changes: 37 additions & 226 deletions recharge/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,55 +1,7 @@
import logging
import time
from enum import Enum
from typing import Any, Literal, Mapping, Optional, Union
from typing import Any, Mapping, Optional, Union

from requests import Request, Response
from requests.exceptions import HTTPError, RequestException, JSONDecodeError
from requests.models import PreparedRequest
from requests.sessions import Session

from ..exceptions import RechargeHTTPError, RechargeAPIError, RechargeRequestException

RechargeVersion = Literal["2021-01", "2021-11"]

RechargeScope = Literal[
"write_orders",
"read_orders",
"read_discounts",
"write_discounts",
"write_subscriptions",
"read_subscriptions",
"write_payments",
"read_payments",
"write_payment_methods",
"read_payment_methods",
"write_customers",
"read_customers",
"write_products",
"read_products",
"store_info",
"write_batches",
"read_batches",
"read_accounts",
"write_checkouts",
"read_checkouts",
"write_notifications",
"read_events",
"write_retention_strategies",
"read_gift_purchases",
"write_gift_purchases",
"read_gift_purchases",
"write_gift_purchases",
"read_bundle_products",
"read_credit_summary",
]


class RequestMethod(Enum):
GET = "GET"
POST = "POST"
PUT = "PUT"
DELETE = "DELETE"
from recharge.client import RechargeClient, RechargeScope, RechargeVersion
from recharge.exceptions import RechargeAPIError


class RechargeResource:
Expand All @@ -69,203 +21,63 @@ class RechargeResource:

def __init__(
self,
session: Session,
logger: Optional[logging.Logger] = None,
client: RechargeClient,
scopes: list[RechargeScope] = [],
max_retries: int = 3,
retry_delay: int = 10,
):
self._session = session
self._logger = logger or logging.getLogger(__name__)
self.scopes = scopes

self._max_retries = max_retries
self._retry_delay = retry_delay
self._retries = 0

self.allowed_endpoints = []

def _redact_auth(self, request: PreparedRequest) -> PreparedRequest:
"""Redacts the Authorization header from a request."""

temp_request = request.copy()
temp_request.headers["X-Recharge-Access-Token"] = "REDACTED"
return temp_request

def _retry(self, request: PreparedRequest) -> Response:
"""Retries a request."""
redacted_request = self._redact_auth(request)
if self._retries >= self._max_retries:
self._logger.error(
"Max retries reached",
extra={
"retries": self._retries,
"max_retries": self._max_retries,
"url": redacted_request.url,
"body": redacted_request.body,
"headers": redacted_request.headers,
},
)
raise RechargeAPIError("Max retries reached")

self._retries += 1
self._logger.info(
"Retrying",
extra={
"retries": self._retries,
"max_retries": self._max_retries,
"delay": self._retry_delay,
"url": redacted_request.url,
"body": redacted_request.body,
"headers": redacted_request.headers,
},
)
time.sleep(self._retry_delay)
return self._send(request)

def _extract_error_message(self, response: Response):
"""Extracts an error message from a response."""
self._client = client
self._scopes = scopes
self._allowed_endpoints = []

try:
error_response = response.json()
return error_response
except JSONDecodeError:
self._logger.error(
"Failed to decode JSON response", extra={"response": response.text}
)
return response.text

def _send(self, request: PreparedRequest) -> Response:
"""Sends a request and handles retries and errors."""

self._logger.debug(
"Sending request", extra={"url": self._redact_auth(request).url}
)
try:
response = self._session.send(request)

if response.status_code == 429:
self._logger.warning(
"Rate limited, retrying...", extra={"response": response.text}
)
return self._retry(request)

if response.status_code >= 500:
self._logger.error(
"Server error, retrying...",
extra={
"response": response.text,
"status_code": response.status_code,
},
)
return self._retry(request)

self._retries = 0
response.raise_for_status()

self._logger.debug("Request successful", extra={"response": response.text})
return response

except HTTPError as http_error:
self._logger.error(
"HTTP error",
extra={
"error": http_error.response.text,
"request": http_error.request,
},
)
raise RechargeHTTPError(
self._extract_error_message(http_error.response)
) from http_error
except RequestException as request_error:
self._logger.error(
"Request failed",
extra={
"error": "An ambiguous error occured",
"request": request_error.request,
},
)
raise RechargeRequestException("Request failed") from request_error

def _extract_data(
self, response: Response, expected: type[Union[dict, list]]
) -> Union[dict, list]:
key = self.object_dict_key if expected is dict else self.object_list_key
try:
response_json = response.json()
except JSONDecodeError:
self._logger.error(
"Failed to decode JSON response, expect missing data",
extra={"response": response.text},
)
return expected()

data = response_json.get(key, response_json)

if not isinstance(data, expected):
raise ValueError(
f"Expected data to be of type {expected.__name__}, got {type(data).__name__}"
)

return data
@property
def _url(self) -> str:
return f"{self.base_url}/{self.object_list_key}"

def check_scopes(self, endpoint: str, scopes: list[RechargeScope]):
if endpoint in self.allowed_endpoints:
def _check_scopes(self, endpoint: str, scopes: list[RechargeScope]):
if endpoint in self._allowed_endpoints:
return

if not self.scopes:
if not self._scopes:
raise RechargeAPIError("No scopes found for token.")

missing_scopes = []

for scope in scopes:
if scope not in self.scopes:
if scope not in self._scopes:
missing_scopes.append(scope)

if missing_scopes:
raise RechargeAPIError(
f"Endpoint {endpoint} missing scopes: {missing_scopes}"
)
else:
self.allowed_endpoints.append(endpoint)

def _request(
self,
method: RequestMethod,
url: str,
query: Optional[Mapping[str, Any]] = None,
json: Optional[Mapping[str, Any]] = None,
) -> Response:
request = Request(method.value, url, params=query, json=json)
prepared_request = self._session.prepare_request(request)
return self._send(prepared_request)
self._allowed_endpoints.append(endpoint)

@property
def url(self) -> str:
return f"{self.base_url}/{self.object_list_key}"

def _update_headers(self):
self._session.headers.update({"X-Recharge-Version": self.recharge_version})
def _get_response_key(self, expected: type[Union[dict, list]]) -> Optional[str]:
if expected is dict:
return self.object_dict_key
elif expected is list:
return self.object_list_key
else:
return None

def _http_delete(
def _http_get(
self,
url: str,
body: Optional[Mapping[str, Any]] = None,
query: Optional[Mapping[str, Any]] = None,
expected: type[Union[dict, list]] = dict,
) -> Union[dict, list]:
self._update_headers()
response = self._request(RequestMethod.DELETE, url, json=body)
return self._extract_data(response, expected)
return self._client.get(url, query, self._get_response_key(expected), expected)

def _http_get(
def _http_post(
self,
url: str,
body: Optional[Mapping[str, Any]] = None,
query: Optional[Mapping[str, Any]] = None,
expected: type[Union[dict, list]] = dict,
) -> Union[dict, list]:
self._update_headers()
response = self._request(RequestMethod.GET, url, query)
return self._extract_data(response, expected)
return self._client.post(
url, body, query, self._get_response_key(expected), expected
)

def _http_put(
self,
Expand All @@ -274,17 +86,16 @@ def _http_put(
query: Optional[Mapping[str, Any]] = None,
expected: type[Union[dict, list]] = dict,
) -> Union[dict, list]:
self._update_headers()
response = self._request(RequestMethod.PUT, url, query, body)
return self._extract_data(response, expected)
return self._client.put(
url, body, query, self._get_response_key(expected), expected
)

def _http_post(
def _http_delete(
self,
url: str,
body: Optional[Mapping[str, Any]] = None,
query: Optional[Mapping[str, Any]] = None,
expected: type[Union[dict, list]] = dict,
) -> Union[dict, list]:
self._update_headers()
response = self._request(RequestMethod.POST, url, query, body)
return self._extract_data(response, expected)
return self._client.delete(
url, body, self._get_response_key(expected), expected
)
Loading

0 comments on commit c3e9af8

Please sign in to comment.