From f53430e21a57686b4d4de9a2af6c425da5accc3b Mon Sep 17 00:00:00 2001 From: Corey Regan Date: Wed, 24 Jan 2024 23:14:58 -0800 Subject: [PATCH] Prices.get shall be our first victim --- setup.py | 2 +- src/Client.py | 111 ++++++++++++------ src/Logger/NullHandler.py | 6 + .../Prices/Operations/List/Includes.py | 5 + src/Resources/Prices/Operations/ListPrices.py | 51 ++++++++ src/Resources/Prices/PricesClient.py | 58 +++++++++ 6 files changed, 197 insertions(+), 36 deletions(-) create mode 100644 src/Logger/NullHandler.py create mode 100644 src/Resources/Prices/Operations/List/Includes.py create mode 100644 src/Resources/Prices/Operations/ListPrices.py create mode 100644 src/Resources/Prices/PricesClient.py diff --git a/setup.py b/setup.py index ec543cd..1c69621 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='paddle-billing-python-sdk', - version='0.0.1a20', + version='0.0.1a21', author='Corey Regan', author_email='regan.corey@gmail.com', description='A Python wrapper for the Paddle Billing API', diff --git a/src/Client.py b/src/Client.py index 9252d28..2138012 100644 --- a/src/Client.py +++ b/src/Client.py @@ -1,30 +1,33 @@ -import requests +from logging import getLogger +from json import dumps as json_dumps +from requests import request, RequestException, Session from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry -from urllib.parse import urljoin, urlencode +from urllib.parse import urljoin, urlencode from uuid import uuid4 -from json import dumps as json_dumps -from urllib.parse import urlparse - +from src.HasParameters import HasParameters +from src.Options import Options # from src.Logger.Formatter import CustomLogger -# from src.Entities.Addresses.AddressesClient import AddressesClient -# from src.Entities.Adjustment.AdjustmentsClient import AdjustmentsClient -# from src.Entities.Businesses.BusinessesClient import BusinessesClient -# from src.Entities.Customers.CustomersClient import CustomersClient -# from src.Entities.Discounts.DiscountsClient import DiscountsClient -# from src.Entities.Events.EventsClient import EventsClient -# from src.Entities.EventTypes.EventTypesClient import EventTypesClient -# from src.Entities.NotificationLogs.NotificationLogsClient import NotificationLogsClient -# from src.Entities.Notifications.NotificationsClient import NotificationsClient -# from src.Entities.NotificationSettings.NotificationSettingsClient import NotificationSettingsClient -# from src.Entities.Prices.PricesClient import PricesClient -# from src.Entities.PricingPreviews.PricingPreviewsClient import PricingPreviewsClient -# from src.Entities.Products.ProductsClient import ProductsClient -# from src.Entities.Reports.ReportsClient import ReportsClient -# from src.Entities.Subscriptions.SubscriptionsClient import SubscriptionsClient -# from src.Entities.Transactions.TransactionsClient import TransactionsClient +from src.Logger.NullHandler import NullHandler + +# from src.Resources.Addresses.AddressesClient import AddressesClient +# from src.Resources.Adjustment.AdjustmentsClient import AdjustmentsClient +# from src.Resources.Businesses.BusinessesClient import BusinessesClient +# from src.Resources.Customers.CustomersClient import CustomersClient +# from src.Resources.Discounts.DiscountsClient import DiscountsClient +# from src.Resources.Events.EventsClient import EventsClient +# from src.Resources.EventTypes.EventTypesClient import EventTypesClient +# from src.Resources.NotificationLogs.NotificationLogsClient import NotificationLogsClient +# from src.Resources.Notifications.NotificationsClient import NotificationsClient +# from src.Resources.NotificationSettings.NotificationSettingsClient import NotificationSettingsClient +from src.Resources.Prices.PricesClient import PricesClient +# from src.Resources.PricingPreviews.PricingPreviewsClient import PricingPreviewsClient +# from src.Resources.Products.ProductsClient import ProductsClient +# from src.Resources.Reports.ReportsClient import ReportsClient +# from src.Resources.Subscriptions.SubscriptionsClient import SubscriptionsClient +# from src.Resources.Transactions.TransactionsClient import TransactionsClient class Client: @@ -32,32 +35,50 @@ class Client: Client for making API requests using Python's requests library. """ - SDK_VERSION = '0.0.1a20' + SDK_VERSION = '0.0.1a21' def __init__( self, api_key: str, # handle our api key class options: dict = None, - http_client: requests = None, + http_client: Session = None, logger = None, retry_count: int = 3, ): - self.api_key = api_key - self.options = options if options else {} - self.logger = logger + self.__api_key = api_key + self.options = options if options else Options() + self.logger = logger if logger else Client.null_logger() self.retry_count = retry_count self.transaction_id = None - self.client = self.build_request_session() + self.client = self.build_request_session() if not http_client else http_client + # TODO # Initialize other clients as needed # self.products = ProductsClient(self) + self.prices = PricesClient(self) # ... Initialize other resource clients here ... + @staticmethod + def null_logger(): + """ + Create a logger instance that logs everything to nowhere + """ + + null_logger = getLogger('null_logger') + null_logger.addHandler(NullHandler()) + + return null_logger + + def logging_hook(self, response, *args, **kwargs): - self.logger.info(f"Request: {response.request.method} {response.request.url}") - self.logger.info(f"Response: {response.status_code} {response.text}") + """ + Requests logs were redirected here and to our custom logger which filters out sensitive data + """ + + self.logger.debug(f"Request: {response.request.method} {response.request.url}") + self.logger.debug(f"Response: {response.status_code} {response.text}") def _make_request( @@ -66,6 +87,10 @@ def _make_request( uri: str, payload: dict | None = None, ): + """ + Makes an actual API call to Paddle + """ + # Parse and update URI with base URL components if necessary if isinstance(uri, str): uri = urljoin(self.options['environment'].base_url, uri) @@ -76,7 +101,7 @@ def _make_request( # Serialize payload to JSON final_json = None - if payload is not None: + if payload: json_payload = json_dumps(payload) final_json = json_payload if json_payload == '[]' else '{}' @@ -86,18 +111,34 @@ def _make_request( response.raise_for_status() return response - except requests.RequestException as e: + except RequestException as e: if self.logger: self.logger.error(f"Request failed: {e}") raise + @staticmethod + def _format_uri_parameters(uri, parameters: HasParameters): + if isinstance(parameters, HasParameters): + parameters = parameters.get_parameters() + + query = urlencode(parameters) + uri += '&' if '?' in uri else '?' + uri += query + + return uri + + def get_raw(self, uri, parameters=None): - return self._make_request('GET', uri, None, parameters) + uri = Client._format_uri_parameters(uri, parameters) if parameters else uri + + return self._make_request('GET', uri, None) def post_raw(self, uri, payload=None, parameters=None): - return self._make_request('POST', uri, payload, parameters) + uri = Client._format_uri_parameters(uri, parameters) if parameters else uri + + return self._make_request('POST', uri, payload) def patch_raw(self, uri, payload): @@ -109,9 +150,9 @@ def delete_raw(self, uri): def build_request_session(self): - session = requests.Session() + session = Session() session.headers.update({ - 'Authorization': f"Bearer {self.api_key}", + 'Authorization': f"Bearer {self.__api_key}", 'Content-Type': 'application/json', 'User-Agent': f"paddle-billing-python-sdk {self.SDK_VERSION}", }) diff --git a/src/Logger/NullHandler.py b/src/Logger/NullHandler.py new file mode 100644 index 0000000..ff3c1fd --- /dev/null +++ b/src/Logger/NullHandler.py @@ -0,0 +1,6 @@ +import logging + + +class NullHandler(logging.Handler): + def emit(self, record): + pass diff --git a/src/Resources/Prices/Operations/List/Includes.py b/src/Resources/Prices/Operations/List/Includes.py new file mode 100644 index 0000000..46550ce --- /dev/null +++ b/src/Resources/Prices/Operations/List/Includes.py @@ -0,0 +1,5 @@ +from enum import StrEnum + + +class Includes(StrEnum): + Product = 'product' diff --git a/src/Resources/Prices/Operations/ListPrices.py b/src/Resources/Prices/Operations/ListPrices.py new file mode 100644 index 0000000..e649b2f --- /dev/null +++ b/src/Resources/Prices/Operations/ListPrices.py @@ -0,0 +1,51 @@ +from src.Entities.Shared.CatalogType import CatalogType +from src.Entities.Shared.Status import Status + +from src.Exceptions.SdkExceptions.InvalidArgumentException import InvalidArgumentException + +from src.Resources.Prices.Operations.List.Includes import Includes + + +class ListPrices: + def __init__(self, pager=None, includes=None, ids=None, types=None, product_ids=None, statuses=None, recurring=None): + self.pager = pager + self.includes = includes if includes is not None else [] + self.ids = ids if ids is not None else [] + self.types = types if types is not None else [] + self.product_ids = product_ids if product_ids is not None else [] + self.statuses = statuses if statuses is not None else [] + self.recurring = recurring + + # Validation + if any(not isinstance(include, Includes) for include in self.includes): + raise InvalidArgumentException('includes', Includes.__name__) + if any(not isinstance(i, str) for i in self.ids): + raise InvalidArgumentException('ids', 'string') + if any(not isinstance(t, CatalogType) for t in self.types): + raise InvalidArgumentException('types', CatalogType.__name__) + if any(not isinstance(pid, str) for pid in self.product_ids): + raise InvalidArgumentException('productIds', 'string') + if any(not isinstance(status, Status) for status in self.statuses): + raise InvalidArgumentException('statuses', Status.__name__) + + + def get_parameters(self): + enum_stringify = lambda enum: enum.value # noqa E731 + + parameters = {} + if self.pager: + parameters.update(self.pager.get_parameters()) + if self.includes: + parameters['include'] = ','.join(map(enum_stringify, self.includes)) + if self.ids: + parameters['id'] = ','.join(self.ids) + if self.types: + parameters['type'] = ','.join(map(enum_stringify, self.types)) + if self.product_ids: + parameters['product_id'] = ','.join(self.product_ids) + if self.statuses: + parameters['status'] = ','.join(map(enum_stringify, self.statuses)) + if self.recurring is not None: + parameters['recurring'] = 'true' if self.recurring else 'false' + + return parameters diff --git a/src/Resources/Prices/PricesClient.py b/src/Resources/Prices/PricesClient.py new file mode 100644 index 0000000..8eca0f3 --- /dev/null +++ b/src/Resources/Prices/PricesClient.py @@ -0,0 +1,58 @@ +from src.ResponseParser import ResponseParser + +from src.Entities.PriceWithIncludes import PriceWithIncludes + +from src.Entities.Collections.Paginator import Paginator +from src.Entities.Collections.PriceWithIncludesCollection import PriceWithIncludesCollection + +from src.Entities.Shared.Status import Status + +# from src.Resources.Prices.Operations.CreatePrice import CreatePrice +from src.Resources.Prices.Operations.ListPrices import ListPrices +# from src.Resources.Prices.Operations.UpdatePrice import UpdatePrice + + +class PricesClient: + def __init__(self, client): + self.client = client + + + def list(self, list_operation: ListPrices = None): + if list_operation is None: + list_operation = ListPrices() + + response = self.client.get_raw('/prices', list_operation.get_parameters()) + parser = ResponseParser(response) + return PriceWithIncludesCollection.from_list(parser.get_data(), Paginator(self.client, parser.get_pagination(), PriceWithIncludesCollection)) + + + def get(self, price_id: str, includes = None): + if includes is None: + includes = [] + + # Validate 'includes' items and build parameters + params = {'include': ','.join(include.value for include in includes)} if includes else {} + response = self.client.get_raw(f"/prices/{price_id}", params) + parser = ResponseParser(response) + + return PriceWithIncludes.from_dict(parser.get_data()) + + + # def create(self, create_operation: CreatePrice): + # response = self.client.post_raw('/prices', create_operation.get_parameters()) + # parser = ResponseParser(response) + # + # return PriceWithIncludes.from_dict(parser.get_data()) + # + # + # def update(self, price_id: str, operation: UpdatePrice): + # response = self.client.patch_raw(f"/prices/{price_id}", operation.get_parameters()) + # parser = ResponseParser(response) + # + # return PriceWithIncludes.from_dict(parser.get_data()) + # + # + # def archive(self, price_id: str): + # operation = UpdatePrice(status=Status.Archived) + # + # return self.update(price_id, operation)