From 81167680530e47cdb4c1c56afebe0c9a50ba6fe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matheus=20Ten=C3=B3rio?= Date: Thu, 21 Mar 2024 14:11:17 -0300 Subject: [PATCH 1/2] 0.3.0 --- .editorconfig | 2 + .flake8 | 6 ++ docs/CHANGELOG.md | 12 ++- hotmart_python/hotmart.py | 162 ++++++++++++++++++++------------ poetry.lock | 54 +++++++++-- pyproject.toml | 7 +- tests/test_helpers.py | 109 +++++++++++++++------- tests/test_sales.py | 178 +++++++++++++++++++++++++++--------- tests/test_subscriptions.py | 26 ++++-- 9 files changed, 403 insertions(+), 153 deletions(-) create mode 100644 .editorconfig create mode 100644 .flake8 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..23da48e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,2 @@ +[*.py] +max_line_length = 100 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..3559e7c --- /dev/null +++ b/.flake8 @@ -0,0 +1,6 @@ + +[flake8] +exclude = + .venv + __init__.py +max-line-length = 100 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 5db8a07..44b457d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,3 +1,12 @@ +0.3.0 / 2024-03-21 +================== + +* Breaking change: Removed support for python <3.8 due to the use of flake8 + for linting. +* Added .editorconfig file for better code standardization. +* Code slightly refactored to comply with flake8. +* Added GitHub Actions for Testing and Linting. + 0.2.2 / 2024-03-21 ================== @@ -11,7 +20,8 @@ * Added subscriptions endpoint * Added tests for subscriptions -* Changed _get_with_token and _post_with_token methods to use the new _request_with_token for more flexibility +* Changed _get_with_token and _post_with_token methods to use the new _request_with_token for more + flexibility * Fixed issue with pagination * Updated docs for the new subscriptions endpoint * Renamed README.dev.md to CONTRIBUTING.md, for better standardization diff --git a/hotmart_python/hotmart.py b/hotmart_python/hotmart.py index fb228e5..ec7e667 100644 --- a/hotmart_python/hotmart.py +++ b/hotmart_python/hotmart.py @@ -42,7 +42,9 @@ def __init__(self, message, status_code, url, response_body=None): self.response_body = response_body def __str__(self): - return f"HTTPRequestException: {self.args[0]}, Status Code: {self.status_code}, URL: {self.url}, Response Body: {self.response_body}" + return (f"HTTPRequestException: {self.args[0]}, " + f"Status Code: {self.status_code}, " + f"URL: {self.url}, Response Body: {self.response_body}") class RequestException(Exception): @@ -60,10 +62,13 @@ def __str__(self): class Hotmart: - def __init__(self, client_id: str, client_secret: str, basic: str, api_version: int = 1, - sandbox: bool = False, log_level: int = logging.CRITICAL) -> None: + def __init__(self, client_id: str, client_secret: str, basic: str, + api_version: int = 1, + sandbox: bool = False, + log_level: int = logging.CRITICAL) -> None: """ - Initializes the Hotmart API client. Full docs can be found at https://developers.hotmart.com/docs/en/ + Initializes the Hotmart API client. Full docs can be found at + https://developers.hotmart.com/docs/en/ :param client_id: The Client ID provided by Hotmart. :param client_secret: The Client Secret provided by Hotmart. :param basic: The Basic Token provided by Hotmart. @@ -90,8 +95,10 @@ def __init__(self, client_id: str, client_secret: str, basic: str, api_version: @staticmethod def _build_payload(**kwargs: Any) -> Dict[str, Any]: """ - Builds a payload with the given kwargs, ignoring the ones with None value - :param kwargs: Expected kwargs can be found in the "Request parameters" section of the API Docs. + Builds a payload with the given kwargs, ignoring the ones + with None value + :param kwargs: Expected kwargs can be found in the "Request parameters" + section of the API Docs. :return: Dict[str, Any]: The built payload as a dictionary. """ payload = {} @@ -105,10 +112,13 @@ def _log_instance_mode(self) -> None: Logs the instance mode (Sandbox or Production). :return: None """ - return self.logger.warning(f"Instance in {'Sandbox' if self.sandbox else 'Production'} mode") + return self.logger.warning( + f"Instance in {'Sandbox' if self.sandbox else 'Production'} mode") - def _make_request(self, method: Any, url: str, headers: Optional[Dict[str, str]] = None, - params: Optional[Dict[str, Any]] = None, body: Optional[Dict[str, str]] = None, + def _make_request(self, method: Any, url: str, + headers: Optional[Dict[str, str]] = None, + params: Optional[Dict[str, Any]] = None, + body: Optional[Dict[str, str]] = None, log_level: int = None) -> Optional[Dict[str, Any]]: """ Makes a request to the given url. @@ -116,12 +126,14 @@ def _make_request(self, method: Any, url: str, headers: Optional[Dict[str, str]] :param url: The URL to make the request to. :param headers: Optional request headers. :param params: Optional request parameters. - :param log_level: The logging level for this method (default is None, inherits from class level). - :return: The JSON response if the request was successful, None otherwise. + :param log_level: The logging level for this method (default is None, + inherits from class level). + :return: The JSON response if the request was successful, + None otherwise. """ if log_level is not None: - logger = logging.getLogger(__name__) + logger = logging.getLogger(__name__) # noqa logger.setLevel(log_level) try: self.logger.debug(f"Request URL: {url}") @@ -137,35 +149,48 @@ def _make_request(self, method: Any, url: str, headers: Optional[Dict[str, str]] except requests.exceptions.HTTPError: # noinspection PyUnboundLocalVariable if response.status_code != 403: - raise HTTPRequestException(f"HTTP Error", response.status_code, url, body) + raise HTTPRequestException("HTTP Error", + response.status_code, + url, + body) error_message = "Forbidden." self.logger.error(f"Error {response.status_code}: {error_message}") if self.sandbox: - self.logger.error("Check if the provided credentials are for Sandbox mode.") + self.logger.error( + "Check if the provided credentials are for Sandbox mode.") - self.logger.error("Perhaps the provided credentials are for Sandbox mode?") - raise HTTPRequestException(error_message, response.status_code, url, body) + self.logger.error( + "Perhaps the provided credentials are for Sandbox mode?") + raise HTTPRequestException(error_message, response.status_code, + url, body) except requests.exceptions.RequestException as e: - error_message = str(e) if str(e) else "An error occurred while making the request" - self.logger.error(f"Error making request to {url}: {error_message}") - raise RequestException(f"Error making request to {url}: {error_message}", url) + error_message = str(e) if str( + e) else "An error occurred while making the request" + self.logger.error( + f"Error making request to {url}: {error_message}") + raise RequestException( + f"Error making request to {url}: {error_message}", + url) def _is_token_expired(self) -> bool: """ Checks if the current token has expired. :return: True if the token has expired, False otherwise. """ - return self.token_expires_at is not None and self.token_expires_at < time.time() + return (self.token_expires_at is not None and self.token_expires_at < + time.time()) def _fetch_new_token(self) -> str: self.logger.info("Fetching a new access token.") method_url = 'https://api-sec-vlc.hotmart.com/security/oauth/token' headers = {'Authorization': self.basic} - payload = {'grant_type': 'client_credentials', 'client_id': self.id, 'client_secret': self.secret} + payload = {'grant_type': 'client_credentials', 'client_id': self.id, + 'client_secret': self.secret} - response = self._make_request(requests.post, method_url, headers=headers, params=payload) + response = self._make_request(requests.post, method_url, + headers=headers, params=payload) self.logger.info("Token obtained successfully") return response['access_token'] @@ -191,12 +216,14 @@ def _get_token(self) -> Optional[str]: def _request_with_token(self, method: str, url: str, body: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """ - Makes an authenticated request (GET, POST, PATCH, etc.) to the specified URL with the given body or params. + Makes an authenticated request (GET, POST, PATCH, etc.) to the + specified URL with the given body or params. :param method: The HTTP method (e.g., 'GET', 'POST', 'PATCH'). :param url: the URL to make the request to. :param body: Optional request body. :param params: Optional request parameters. - :return: The JSON Response if successful, otherwise raises an exception. + :return: The JSON Response if successful, + otherwise raises an exception. """ token = self._get_token() @@ -214,16 +241,19 @@ def _request_with_token(self, method: str, url: str, body: Optional[Dict[str, An if method.upper() not in method_mapping: raise ValueError(f"Unsupported method: {method}") - return self._make_request(method_mapping[method.upper()], url, headers=headers, params=params, body=body) + return self._make_request(method_mapping[method.upper()], url, headers=headers, + params=params, body=body) - def _pagination(self, method: str, url: str, params: Optional[Dict[str, Any]] = None, paginate: bool = False) -> \ - Optional[List[Dict[str, Any]]]: + def _pagination(self, method: str, url: str, params: Optional[Dict[str, Any]] = None, + paginate: bool = False) -> Optional[List[Dict[str, Any]]]: """ Retrieves all pages of data for a paginated endpoint. :param url: The URL of the paginated endpoint. :param params: Optional request parameters. - :param paginate: Whether to paginate the results or not (default is False). - :return: A list containing data from all pages, or None if an error occurred. + :param paginate: Whether to paginate the results or not + (default is False). + :return: A list containing data from all pages, + or None if an error occurred. """ if not paginate: response = self._request_with_token(method=method, url=url, params=params) @@ -245,7 +275,8 @@ def _pagination(self, method: str, url: str, params: Optional[Dict[str, Any]] = response = self._request_with_token(method, url, params=params) if response is None: - raise ValueError(f"Failed to fetch next page with token: {next_page_token}") + raise ValueError( + f"Failed to fetch next page with token: {next_page_token}") all_items.extend(response.get("items", [])) @@ -255,8 +286,9 @@ def _pagination(self, method: str, url: str, params: Optional[Dict[str, Any]] = def get_sales_history(self, paginate: bool = False, **kwargs: Any) -> Optional[Dict[str, Any]]: """ Retrieves sales history data based on the provided filters. - :param kwargs: Filters to apply on the request. Expected kwargs can be found in the "Request parameters" - section of the API Docs. + + :param kwargs: Filters to apply on the request. Expected kwargs can be found in + the "Request parameters" section of the API Docs. :param paginate: Whether to paginate the results or not (default is False). :return: Sales history data if available, otherwise None. """ @@ -264,14 +296,15 @@ def get_sales_history(self, paginate: bool = False, **kwargs: Any) -> Optional[D method = "get" url = f'{self.base_url}/sales/history' payload = self._build_payload(**kwargs) - return self._pagination(method=method, url=url, params=payload, paginate=paginate) + return self._pagination(method=method, url=url, params=payload, + paginate=paginate) def get_sales_summary(self, paginate: bool = False, **kwargs: Any) -> Optional[Dict[str, Any]]: """ Retrieves sales summary data based on the provided filters. - :param kwargs: Filters to apply on the request. Expected kwargs can be found in the "Request parameters" - section of the API Docs. + :param kwargs: Filters to apply on the request. Expected kwargs can be found in the + "Request parameters" section of the API Docs. :param paginate: Whether to paginate the results or not (default is False). :return: Sales summary data if available, otherwise None. """ @@ -280,14 +313,16 @@ def get_sales_summary(self, paginate: bool = False, **kwargs: Any) -> Optional[D method = "get" url = f'{self.base_url}/sales/summary' payload = self._build_payload(**kwargs) - return self._pagination(method=method, url=url, params=payload, paginate=paginate) + return self._pagination(method=method, url=url, params=payload, + paginate=paginate) - def get_sales_participants(self, paginate: bool = True, **kwargs: Any) -> Optional[Dict[str, Any]]: + def get_sales_participants(self, paginate: bool = True, **kwargs: Any) -> \ + Optional[Dict[str, Any]]: """ Retrieves sales user data based on the provided filters. - :param kwargs: Filters to apply on the request. Expected kwargs can be found in the "Request parameters" - section of the API Docs. + :param kwargs: Filters to apply on the request. Expected kwargs can be found in the + "Request parameters" section of the API Docs. :param paginate: Whether to paginate the results or not (default is True). :return: Sales user data if available, otherwise None. """ @@ -298,12 +333,13 @@ def get_sales_participants(self, paginate: bool = True, **kwargs: Any) -> Option payload = self._build_payload(**kwargs) return self._pagination(method=method, url=url, params=payload, paginate=paginate) - def get_sales_commissions(self, paginate: bool = False, **kwargs: Any) -> Optional[Dict[str, Any]]: + def get_sales_commissions(self, paginate: bool = False, **kwargs: Any) -> \ + Optional[Dict[str, Any]]: """ Retrieves sales commissions data based on the provided filters. - :param kwargs: Filters to apply on the request. Expected kwargs can be found in the "Request parameters" - section of the API Docs. + :param kwargs: Filters to apply on the request. Expected kwargs can be found in the + "Request parameters" section of the API Docs. :param paginate: Whether to paginate the results or not (default is False). :return: Sales commissions data if available, otherwise None. """ @@ -314,12 +350,13 @@ def get_sales_commissions(self, paginate: bool = False, **kwargs: Any) -> Option payload = self._build_payload(**kwargs) return self._pagination(method=method, url=url, params=payload, paginate=paginate) - def get_sales_price_details(self, paginate: bool = False, **kwargs: Any) -> Optional[Dict[str, Any]]: + def get_sales_price_details(self, paginate: bool = False, **kwargs: Any) \ + -> Optional[Dict[str, Any]]: """ Retrieves sales price details based on the provided filters. - :param kwargs: Filters to apply on the request. Expected kwargs can be found in the "Request parameters" - section of the API Docs. + :param kwargs: Filters to apply on the request. Expected kwargs can be found in the + "Request parameters" section of the API Docs. :param paginate: Whether to paginate the results or not (default is False). :return: Sales price details if available, otherwise None. """ @@ -330,12 +367,13 @@ def get_sales_price_details(self, paginate: bool = False, **kwargs: Any) -> Opti payload = self._build_payload(**kwargs) return self._pagination(method=method, url=url, params=payload, paginate=paginate) - def get_subscriptions(self, paginate: bool = False, **kwargs: Any) -> Optional[Dict[str, Any]]: + def get_subscriptions(self, paginate: bool = False, **kwargs: Any) -> \ + Optional[Dict[str, Any]]: """ Retrieves subscription data based on the provided filters. - :param kwargs: Filters to apply on the request. Expected kwargs can be found in the "Request parameters" - section of the API Docs. + :param kwargs: Filters to apply on the request. Expected kwargs can be found in the + "Request parameters" section of the API Docs. :param paginate: Whether to paginate the results or not (default is False). :return: Subscription data if available, otherwise None. """ @@ -346,12 +384,13 @@ def get_subscriptions(self, paginate: bool = False, **kwargs: Any) -> Optional[D payload = self._build_payload(**kwargs) return self._pagination(method=method, url=url, params=payload, paginate=paginate) - def get_subscriptions_summary(self, paginate: bool = False, **kwargs: Any) -> Optional[Dict[str, Any]]: + def get_subscriptions_summary(self, paginate: bool = False, **kwargs: Any) -> \ + Optional[Dict[str, Any]]: """ Retrieves subscription summary data based on the provided filters. :param paginate: Whether to paginate the results or not (default is False). - :param kwargs: Filters to apply on the request. Expected kwargs can be found in the "Request parameters" - section of the API Docs. + :param kwargs: Filters to apply on the request. Expected kwargs can be found in the + "Request parameters" section of the API Docs. :return: Subscription data if available, otherwise None. """ @@ -361,14 +400,14 @@ def get_subscriptions_summary(self, paginate: bool = False, **kwargs: Any) -> Op payload = self._build_payload(**kwargs) return self._pagination(method=method, url=url, params=payload, paginate=paginate) - def get_subscription_purchases(self, subscriber_code, paginate: bool = False, **kwargs: Any) -> Optional[ - Dict[str, Any]]: + def get_subscription_purchases(self, subscriber_code, paginate: bool = False, **kwargs: Any) ->\ + Optional[Dict[str, Any]]: """ Retrieves subscription purchases data based on the provided filters. :param subscriber_code: The subscriber code to filter the request. - :param kwargs: Filters to apply on the request. Expected kwargs can be found in the "Request parameters" - section of the API Docs. + :param kwargs: Filters to apply on the request. Expected kwargs can be found in the + "Request parameters" section of the API Docs. :param paginate: Whether to paginate the results or not (default is False). :return: Subscription purchases data if available, otherwise None. """ @@ -379,9 +418,11 @@ def get_subscription_purchases(self, subscriber_code, paginate: bool = False, ** payload = self._build_payload(**kwargs) return self._pagination(method=method, url=url, params=payload, paginate=paginate) - def cancel_subscription(self, subscriber_code: list[str], send_email: bool = True) -> Optional[Dict[str, Any]]: + def cancel_subscription(self, subscriber_code: list[str], send_email: bool = True) ->\ + Optional[Dict[str, Any]]: """ Cancels a subscription. + :param subscriber_code: The subscriber code you want to cancel the subscription :param send_email: Whether to email the subscriber or not (default is True). :return: @@ -396,11 +437,13 @@ def cancel_subscription(self, subscriber_code: list[str], send_email: bool = Tru } return self._request_with_token(method=method, url=url, body=payload) - def reactivate_and_charge_subscription(self, subscriber_code: list[str], charge: bool = False) -> Optional[ - Dict[str, Any]]: + def reactivate_and_charge_subscription(self, subscriber_code: list[str], charge: bool = False)\ + -> Optional[Dict[str, Any]]: """ Reactivates and charges a subscription. - :param subscriber_code: The subscriber code you want to reactivate and charge the subscription + + :param subscriber_code: The subscriber code you want to reactivate + and charge the subscription :param charge: Whether to make a new charge to the subscriber or not (default is False). :return: """ @@ -417,6 +460,7 @@ def reactivate_and_charge_subscription(self, subscriber_code: list[str], charge: def change_due_day(self, subscriber_code: str, new_due_day: int) -> Optional[Dict[str, Any]]: """ Changes the due day of a subscription. + :param subscriber_code: The subscriber code you want to change the due day :param new_due_day: The new due day you want to set :return: diff --git a/poetry.lock b/poetry.lock index 85ffca7..f1ebd7f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -127,6 +127,22 @@ humanfriendly = ">=9.1" [package.extras] cron = ["capturer (>=2.4)"] +[[package]] +name = "flake8" +version = "7.0.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8-7.0.0-py2.py3-none-any.whl", hash = "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3"}, + {file = "flake8-7.0.0.tar.gz", hash = "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.11.0,<2.12.0" +pyflakes = ">=3.2.0,<3.3.0" + [[package]] name = "humanfriendly" version = "10.0" @@ -139,7 +155,6 @@ files = [ ] [package.dependencies] -pyreadline = {version = "*", markers = "sys_platform == \"win32\" and python_version < \"3.8\""} pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""} [[package]] @@ -154,13 +169,36 @@ files = [ ] [[package]] -name = "pyreadline" -version = "2.1" -description = "A python implmementation of GNU readline." +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" optional = false -python-versions = "*" +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "pycodestyle" +version = "2.11.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, + {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, +] + +[[package]] +name = "pyflakes" +version = "3.2.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.8" files = [ - {file = "pyreadline-2.1.zip", hash = "sha256:4530592fc2e85b25b1a9f79664433da09237c1a270e4d78ea5aa3a2c7229e2d1"}, + {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, + {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, ] [[package]] @@ -214,5 +252,5 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.0" -python-versions = "^3.7" -content-hash = "46cdf135b68ea62e3b777a9761269b06f654c507d8255c62d1a5041376c873a8" +python-versions = "^3.8.1" +content-hash = "90a1b3339d4ecea63ab1cfbed90d4c4525e683ae5e8b5a31ff49df6b3b3aa30c" diff --git a/pyproject.toml b/pyproject.toml index c584138..4f8cdf5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,17 +1,20 @@ [tool.poetry] name = "hotmart-python" -version = "0.2.2" +version = "0.3.0" description = "A Python library for the Hotmart API, simplifying endpoint access and resource management." authors = ["Matheus TenĂ³rio "] license = "Apache License, Version 2.0" readme = "docs/README.md" [tool.poetry.dependencies] -python = "^3.7" +python = "^3.8.1" coloredlogs = "^15.0.1" requests = "^2.31.0" +[tool.poetry.group.dev.dependencies] +flake8 = "^7.0.0" + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 3d9904d..0f84bf6 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -3,14 +3,17 @@ from unittest.mock import patch from hotmart_python import Hotmart, RequestException, HTTPRequestException +client_id = 'b32450c1-1352-246a-b6d3-d49d6db815ea' +client_secret = '90bcc221-cebd-5a5b-00e2-72cab47d9282' +basic = ('Basic YjIzNTQxYzAtMyEzNS20MjVhLWI1ZDItZDM4ZDVkYjcwNGVhOjA5Y2JiMTEz' + 'LWRiZWMtNGI0YS05OWUxLTI3Y2FiNDdkOTI4Mg==') + class TestHotmart(unittest.TestCase): def setUp(self): - self.hotmart = Hotmart(client_id='b32450c1-1352-246a-b6d3-d49d6db815ea', - client_secret='90bcc221-cebd-5a5b-00e2-72cab47d9282', - basic='Basic ' - 'YjIzNTQxYzAtMyEzNS20MjVhLWI1ZDItZDM4ZDVkYjcwNGVhOjA5Y2JiMTEzLWRiZWMtNGI0YS05OWUx' - 'LTI3Y2FiNDdkOTI4Mg==') + self.hotmart = Hotmart(client_id=client_id, + client_secret=client_secret, + basic=basic) # Build Payload def test_build_payload_with_all_values(self): @@ -27,12 +30,14 @@ def test_build_payload_with_no_values(self): # Sandbox Mode def test_sandbox_mode_true(self): - hotmart = Hotmart(client_id='123', client_secret='123', basic='123', sandbox=True) + hotmart = Hotmart(client_id='123', client_secret='123', basic='123', + sandbox=True) self.assertTrue(hotmart.sandbox) def test_sandbox_mode_false(self): hotmart1 = Hotmart(client_id='123', client_secret='123', basic='123') - hotmart2 = Hotmart(client_id='123', client_secret='123', basic='123', sandbox=False) + hotmart2 = Hotmart(client_id='123', client_secret='123', basic='123', + sandbox=False) self.assertFalse(hotmart1.sandbox) self.assertFalse(hotmart2.sandbox) @@ -41,12 +46,15 @@ def test_sandbox_mode_false(self): def test_successful_request(self, mock_get): mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = {"success": True} - response = self.hotmart._make_request(requests.get, 'https://example.com') + response = self.hotmart._make_request(requests.get, + 'https://example.com') self.assertEqual(response, {"success": True}) @patch('requests.get') def test_http_error_request(self, mock_get): - mock_get.return_value.raise_for_status.side_effect = requests.exceptions.HTTPError + mock_get.return_value.raise_for_status.side_effect = ( + requests.exceptions.HTTPError) + with self.assertRaises(HTTPRequestException): self.hotmart._make_request(requests.get, 'https://example.com') @@ -60,7 +68,9 @@ def test_request_exception(self, mock_get): def test_forbidden_request_in_sandbox_mode(self, mock_get): self.hotmart.sandbox = True mock_get.return_value.status_code = 403 - mock_get.return_value.raise_for_status.side_effect = requests.exceptions.HTTPError + mock_get.return_value.raise_for_status.side_effect = ( + requests.exceptions.HTTPError) + with self.assertRaises(HTTPRequestException): self.hotmart._make_request(requests.get, 'https://example.com') @@ -68,7 +78,9 @@ def test_forbidden_request_in_sandbox_mode(self, mock_get): def test_forbidden_request_in_production_mode(self, mock_get): self.hotmart.sandbox = False mock_get.return_value.status_code = 403 - mock_get.return_value.raise_for_status.side_effect = requests.exceptions.HTTPError + mock_get.return_value.raise_for_status.side_effect = ( + requests.exceptions.HTTPError) + with self.assertRaises(HTTPRequestException): self.hotmart._make_request(requests.get, 'https://example.com') @@ -86,7 +98,8 @@ def test_token_not_expired_when_expiry_in_future(self, mock_time): @patch('requests.post') def test_token_obtained_successfully(self, mock_post): - mock_post.return_value.json.return_value = {'access_token': 'test_token'} + mock_post.return_value.json.return_value = { + 'access_token': 'test_token'} token = self.hotmart._fetch_new_token() self.assertEqual(token, 'test_token') @@ -98,7 +111,8 @@ def test_token_obtained_failure(self, mock_post): @patch.object(Hotmart, '_is_token_expired') @patch.object(Hotmart, '_fetch_new_token') - def test_token_found_in_cache(self, mock_fetch_new_token, mock_is_token_expired): + def test_token_found_in_cache(self, mock_fetch_new_token, + mock_is_token_expired): mock_is_token_expired.return_value = False self.hotmart.token_cache = 'test_token' token = self.hotmart._get_token() @@ -107,7 +121,9 @@ def test_token_found_in_cache(self, mock_fetch_new_token, mock_is_token_expired) @patch.object(Hotmart, '_is_token_expired') @patch.object(Hotmart, '_fetch_new_token') - def test_token_not_in_cache_and_fetched_successfully(self, mock_fetch_new_token, mock_is_token_expired): + def test_token_not_in_cache_and_fetched_success(self, + mock_fetch_new_token, + mock_is_token_expired): mock_is_token_expired.return_value = True mock_fetch_new_token.return_value = 'new_token' token = self.hotmart._get_token() @@ -115,7 +131,8 @@ def test_token_not_in_cache_and_fetched_successfully(self, mock_fetch_new_token, @patch.object(Hotmart, '_is_token_expired') @patch.object(Hotmart, '_fetch_new_token') - def test_token_not_in_cache_and_fetch_failed(self, mock_fetch_new_token, mock_is_token_expired): + def test_token_not_in_cache_and_fetch_failed(self, mock_fetch_new_token, + mock_is_token_expired): mock_is_token_expired.return_value = True mock_fetch_new_token.return_value = None token = self.hotmart._get_token() @@ -123,54 +140,84 @@ def test_token_not_in_cache_and_fetch_failed(self, mock_fetch_new_token, mock_is @patch.object(Hotmart, '_get_token') @patch.object(Hotmart, '_make_request') - def test_successful_request_with_token(self, mock_make_request, mock_get_token): + def test_successful_request_with_token(self, mock_make_request, + mock_get_token): mock_get_token.return_value = 'test_token' mock_make_request.return_value = {"success": True} - result = self.hotmart._request_with_token('GET', 'https://example.com') + result = self.hotmart._request_with_token('GET', + 'https://example.com') self.assertEqual(result, {"success": True}) @patch.object(Hotmart, '_get_token') @patch.object(Hotmart, '_make_request') - def test_failed_request_with_token(self, mock_make_request, mock_get_token): + def test_failed_request_with_token(self, mock_make_request, + mock_get_token): mock_get_token.return_value = 'test_token' - mock_make_request.side_effect = RequestException("Error", "url") + mock_make_request.side_effect = RequestException("Error", + "url") with self.assertRaises(RequestException): - self.hotmart._request_with_token('GET', 'https://example.com') + self.hotmart._request_with_token('GET', + 'https://example.com') @patch.object(Hotmart, '_get_token') - @patch.object(Hotmart, '_make_request') - def test_unsupported_method_with_token(self, mock_make_request, mock_get_token): + def test_unsupported_method_with_token(self, + mock_get_token): mock_get_token.return_value = 'test_token' with self.assertRaises(ValueError): - self.hotmart._request_with_token('PUT', 'https://example.com') + self.hotmart._request_with_token('PUT', + 'https://example.com') @patch.object(Hotmart, '_request_with_token') def test_pagination_without_pagination(self, mock_request_with_token): - mock_request_with_token.return_value = {"items": ["item1", "item2"]} - result = self.hotmart._pagination('GET', 'https://example.com') + mock_request_with_token.return_value = { + "items": ["item1", "item2"] + } + result = self.hotmart._pagination('GET', + 'https://example.com') + self.assertEqual(result, ["item1", "item2"]) @patch.object(Hotmart, '_request_with_token') def test_pagination_with_single_page(self, mock_request_with_token): - mock_request_with_token.return_value = {"items": ["item1", "item2"], "page_info": {}} - result = self.hotmart._pagination('GET', 'https://example.com', paginate=True) + mock_request_with_token.return_value = { + "items": ["item1", "item2"], + "page_info": {} + } + + result = self.hotmart._pagination('GET', + 'https://example.com', + paginate=True) + self.assertEqual(result, ["item1", "item2"]) @patch.object(Hotmart, '_request_with_token') def test_pagination_with_multiple_pages(self, mock_request_with_token): mock_request_with_token.side_effect = [ - {"items": ["item1", "item2"], "page_info": {"next_page_token": "token"}}, - {"items": ["item3", "item4"], "page_info": {}} + { + "items": ["item1", "item2"], + "page_info": {"next_page_token": "token"} + }, + { + "items": ["item3", "item4"], + "page_info": {} + } ] params = {} - result = self.hotmart._pagination('GET', 'https://example.com', params=params, paginate=True) + result = self.hotmart._pagination('GET', + 'https://example.com', + params=params, + paginate=True) + self.assertEqual(result, ["item1", "item2", "item3", "item4"]) @patch.object(Hotmart, '_request_with_token') def test_pagination_with_failed_first_page(self, mock_request_with_token): mock_request_with_token.return_value = None + with self.assertRaises(ValueError): - self.hotmart._pagination('GET', 'https://example.com', paginate=True) + self.hotmart._pagination('GET', + 'https://example.com', + paginate=True) if __name__ == '__main__': diff --git a/tests/test_sales.py b/tests/test_sales.py index e58d43a..98c0340 100644 --- a/tests/test_sales.py +++ b/tests/test_sales.py @@ -2,14 +2,17 @@ from unittest.mock import patch from hotmart_python import Hotmart +client_id = 'b32450c1-1352-246a-b6d3-d49d6db815ea' +client_secret = '90bcc221-cebd-5a5b-00e2-72cab47d9282' +basic = ('Basic YjIzNTQxYzAtMyEzNS20MjVhLWI1ZDItZDM4ZDVkYjcwNGVhOjA5Y2JiMTEzLWRiZWMtNGI0YS05OWUxLT\ +I3Y2FiNDdkOTI4Mg==') + class TestHotmart(unittest.TestCase): def setUp(self): - self.hotmart = Hotmart(client_id='b32450c1-1352-246a-b6d3-d49d6db815ea', - client_secret='90bcc221-cebd-5a5b-00e2-72cab47d9282', - basic='Basic ' - 'YjIzNTQxYzAtMyEzNS20MjVhLWI1ZDItZDM4ZDVkYjcwNGVhOjA5Y2JiMTEzLWRiZWMtNGI0YS05OWUx' - 'LTI3Y2FiNDdkOTI4Mg==') + self.hotmart = Hotmart(client_id=client_id, + client_secret=client_secret, + basic=basic) @patch.object(Hotmart, '_build_payload') @patch.object(Hotmart, '_pagination') @@ -17,11 +20,12 @@ def test_get_sales_history_with_pagination(self, mock_pagination, mock_build_pay mock_build_payload.return_value = {"transaction_status": "approved"} mock_pagination.return_value = [{"id": 1}, {"id": 2}] result = self.hotmart.get_sales_history(paginate=True, transaction_status="approved") + self.assertEqual(result, [{"id": 1}, {"id": 2}]) @patch.object(Hotmart, '_build_payload') @patch.object(Hotmart, '_pagination') - def test_get_sales_history_without_pagination(self, mock_pagination, mock_build_payload): + def test_get_sales_history_no_pagination(self, mock_pagination, mock_build_payload): mock_build_payload.return_value = {"filter1": "value1"} mock_pagination.return_value = [{"id": 1}, {"id": 2}] result = self.hotmart.get_sales_history(paginate=False, filter1="value1") @@ -33,100 +37,188 @@ def test_get_sales_history_with_failed_pagination(self, mock_pagination, mock_bu mock_build_payload.return_value = {"filter1": "value1"} mock_pagination.return_value = None result = self.hotmart.get_sales_history(paginate=True, filter1="value1") + self.assertIsNone(result) @patch.object(Hotmart, '_build_payload') @patch.object(Hotmart, '_pagination') def test_get_sales_summary_with_pagination(self, mock_pagination, mock_build_payload): - mock_build_payload.return_value = {"filter1": "value1"} - mock_pagination.return_value = [{"id": 1}, {"id": 2}] + mock_build_payload.return_value = { + "filter1": "value1" + } + mock_pagination.return_value = [ + {"id": 1}, + {"id": 2} + ] result = self.hotmart.get_sales_summary(paginate=True, filter1="value1") - self.assertEqual(result, [{"id": 1}, {"id": 2}]) + + self.assertEqual(result, [ + {"id": 1}, + {"id": 2} + ]) @patch.object(Hotmart, '_build_payload') @patch.object(Hotmart, '_pagination') - def test_get_sales_summary_without_pagination(self, mock_pagination, mock_build_payload): - mock_build_payload.return_value = {"filter1": "value1"} - mock_pagination.return_value = [{"id": 1}, {"id": 2}] + def test_get_sales_summary_no_pagination(self, mock_pagination, mock_build_payload): + mock_build_payload.return_value = { + "filter1": "value1" + } + mock_pagination.return_value = [ + {"id": 1}, + {"id": 2} + ] result = self.hotmart.get_sales_summary(paginate=False, filter1="value1") - self.assertEqual(result, [{"id": 1}, {"id": 2}]) + + self.assertEqual(result, [ + {"id": 1}, + {"id": 2} + ]) @patch.object(Hotmart, '_build_payload') @patch.object(Hotmart, '_pagination') def test_get_sales_summary_with_failed_pagination(self, mock_pagination, mock_build_payload): - mock_build_payload.return_value = {"filter1": "value1"} + mock_build_payload.return_value = { + "filter1": "value1" + } mock_pagination.return_value = None result = self.hotmart.get_sales_summary(paginate=True, filter1="value1") + self.assertIsNone(result) @patch.object(Hotmart, '_build_payload') @patch.object(Hotmart, '_pagination') - def test_sales_participants_retrieval_with_pagination(self, mock_pagination, mock_build_payload): - mock_build_payload.return_value = {"filter1": "value1"} - mock_pagination.return_value = [{"id": 1}, {"id": 2}] + def test_sales_participants_retrieval_with_pagination(self, mock_pagination, + mock_build_payload): + mock_build_payload.return_value = { + "filter1": "value1" + } + mock_pagination.return_value = [ + {"id": 1}, + {"id": 2} + ] result = self.hotmart.get_sales_participants(paginate=True, filter1="value1") - self.assertEqual(result, [{"id": 1}, {"id": 2}]) + + self.assertEqual(result, [ + {"id": 1}, + {"id": 2} + ]) @patch.object(Hotmart, '_build_payload') @patch.object(Hotmart, '_pagination') - def test_sales_participants_retrieval_without_pagination(self, mock_pagination, mock_build_payload): - mock_build_payload.return_value = {"filter1": "value1"} - mock_pagination.return_value = [{"id": 1}, {"id": 2}] + def test_sales_participants_retrieval_no_pagination(self, mock_pagination, mock_build_payload): + mock_build_payload.return_value = { + "filter1": "value1" + } + mock_pagination.return_value = [ + {"id": 1}, + {"id": 2} + ] result = self.hotmart.get_sales_participants(paginate=False, filter1="value1") - self.assertEqual(result, [{"id": 1}, {"id": 2}]) + + self.assertEqual(result, [ + {"id": 1}, + {"id": 2} + ]) @patch.object(Hotmart, '_build_payload') @patch.object(Hotmart, '_pagination') - def test_sales_participants_retrieval_with_failed_pagination(self, mock_pagination, mock_build_payload): - mock_build_payload.return_value = {"filter1": "value1"} + def test_sales_participants_retrieval_failed_pagination(self, mock_pagination, + mock_build_payload): + mock_build_payload.return_value = { + "filter1": "value1" + } mock_pagination.return_value = None result = self.hotmart.get_sales_participants(paginate=True, filter1="value1") + self.assertIsNone(result) @patch.object(Hotmart, '_build_payload') @patch.object(Hotmart, '_pagination') def test_sales_commissions_retrieval_with_pagination(self, mock_pagination, mock_build_payload): - mock_build_payload.return_value = {"filter1": "value1"} - mock_pagination.return_value = [{"id": 1}, {"id": 2}] + mock_build_payload.return_value = { + "filter1": "value1" + } + mock_pagination.return_value = [ + {"id": 1}, + {"id": 2} + ] result = self.hotmart.get_sales_commissions(paginate=True, filter1="value1") - self.assertEqual(result, [{"id": 1}, {"id": 2}]) + + self.assertEqual(result, [ + {"id": 1}, + {"id": 2} + ]) @patch.object(Hotmart, '_build_payload') @patch.object(Hotmart, '_pagination') - def test_sales_commissions_retrieval_without_pagination(self, mock_pagination, mock_build_payload): - mock_build_payload.return_value = {"filter1": "value1"} - mock_pagination.return_value = [{"id": 1}, {"id": 2}] + def test_sales_commissions_retrieval_no_pagination(self, mock_pagination, mock_build_payload): + mock_build_payload.return_value = { + "filter1": "value1" + } + mock_pagination.return_value = [ + {"id": 1}, + {"id": 2} + ] result = self.hotmart.get_sales_commissions(paginate=False, filter1="value1") - self.assertEqual(result, [{"id": 1}, {"id": 2}]) + + self.assertEqual(result, [ + {"id": 1}, + {"id": 2} + ]) @patch.object(Hotmart, '_build_payload') @patch.object(Hotmart, '_pagination') - def test_sales_commissions_retrieval_with_failed_pagination(self, mock_pagination, mock_build_payload): - mock_build_payload.return_value = {"filter1": "value1"} + def test_sales_commissions_retrieval_failed_pagination(self, mock_pagination, + mock_build_payload): + mock_build_payload.return_value = { + "filter1": "value1" + } mock_pagination.return_value = None result = self.hotmart.get_sales_commissions(paginate=True, filter1="value1") + self.assertIsNone(result) @patch.object(Hotmart, '_build_payload') @patch.object(Hotmart, '_pagination') - def test_sales_price_details_retrieval_with_pagination(self, mock_pagination, mock_build_payload): - mock_build_payload.return_value = {"filter1": "value1"} - mock_pagination.return_value = [{"id": 1}, {"id": 2}] + def test_sales_price_details_retrieval_with_pagination(self, mock_pagination, + mock_build_payload): + mock_build_payload.return_value = { + "filter1": "value1" + } + mock_pagination.return_value = [ + {"id": 1}, + {"id": 2} + ] result = self.hotmart.get_sales_price_details(paginate=True, filter1="value1") - self.assertEqual(result, [{"id": 1}, {"id": 2}]) + self.assertEqual(result, [ + {"id": 1}, + {"id": 2} + ]) @patch.object(Hotmart, '_build_payload') @patch.object(Hotmart, '_pagination') - def test_sales_price_details_retrieval_without_pagination(self, mock_pagination, mock_build_payload): - mock_build_payload.return_value = {"filter1": "value1"} - mock_pagination.return_value = [{"id": 1}, {"id": 2}] + def test_sales_price_details_retrieval_no_pagination(self, mock_pagination, mock_build_payload): + mock_build_payload.return_value = { + "filter1": "value1" + } + mock_pagination.return_value = [ + {"id": 1}, + {"id": 2} + ] result = self.hotmart.get_sales_price_details(paginate=False, filter1="value1") - self.assertEqual(result, [{"id": 1}, {"id": 2}]) + self.assertEqual(result, [ + {"id": 1}, + {"id": 2} + ]) @patch.object(Hotmart, '_build_payload') @patch.object(Hotmart, '_pagination') - def test_sales_price_details_retrieval_with_failed_pagination(self, mock_pagination, mock_build_payload): - mock_build_payload.return_value = {"filter1": "value1"} + def test_sales_price_details_retrieval_failed_pagination(self, mock_pagination, + mock_build_payload): + mock_build_payload.return_value = { + "filter1": "value1" + } mock_pagination.return_value = None result = self.hotmart.get_sales_price_details(paginate=True, filter1="value1") + self.assertIsNone(result) diff --git a/tests/test_subscriptions.py b/tests/test_subscriptions.py index f13116d..61cd6e1 100644 --- a/tests/test_subscriptions.py +++ b/tests/test_subscriptions.py @@ -2,14 +2,17 @@ from unittest.mock import patch from hotmart_python import Hotmart, RequestException +client_id = 'b32450c1-1352-246a-b6d3-d49d6db815ea' +client_secret = '90bcc221-cebd-5a5b-00e2-72cab47d9282' +basic = ('Basic YjIzNTQxYzAtMyEzNS20MjVhLWI1ZDItZDM4ZDVkYjcwNGVhOjA5Y2JiMTEz' + 'LWRiZWMtNGI0YS05OWUxLTI3Y2FiNDdkOTI4Mg==') + class TestHotmart(unittest.TestCase): def setUp(self): - self.hotmart = Hotmart(client_id='b32450c1-1352-246a-b6d3-d49d6db815ea', - client_secret='90bcc221-cebd-5a5b-00e2-72cab47d9282', - basic='Basic ' - 'YjIzNTQxYzAtMyEzNS20MjVhLWI1ZDItZDM4ZDVkYjcwNGVhOjA5Y2JiMTEzLWRiZWMtNGI0YS05OWUx' - 'LTI3Y2FiNDdkOTI4Mg==') + self.hotmart = Hotmart(client_id=client_id, + client_secret=client_secret, + basic=basic) @patch.object(Hotmart, '_request_with_token') def should_retrieve_subscriptions_when_valid_request(self, mock_request_with_token): @@ -30,7 +33,8 @@ def should_retrieve_subscription_summary_when_valid_request(self, mock_request_w self.assertEqual(result, {"summary": {"total": 10}}) @patch.object(Hotmart, '_request_with_token') - def should_raise_exception_when_retrieving_subscription_summary_fails(self, mock_request_with_token): + def should_raise_exception_when_retrieving_subscription_summary_fails(self, + mock_request_with_token): mock_request_with_token.side_effect = RequestException("Error", "url") with self.assertRaises(RequestException): self.hotmart.get_subscriptions_summary(paginate=False) @@ -42,7 +46,9 @@ def should_retrieve_subscription_purchases_when_valid_request(self, mock_request self.assertEqual(result, {"purchases": [{"id": 1}, {"id": 2}]}) @patch.object(Hotmart, '_request_with_token') - def should_raise_exception_when_retrieving_subscription_purchases_fails(self, mock_request_with_token): + def should_raise_exception_when_retrieving_subscription_purchases_fails(self, + mock_request_with_token + ): mock_request_with_token.side_effect = RequestException("Error", "url") with self.assertRaises(RequestException): self.hotmart.get_subscription_purchases(subscriber_code="123", paginate=False) @@ -62,11 +68,13 @@ def should_raise_exception_when_cancelling_subscription_fails(self, mock_request @patch.object(Hotmart, '_request_with_token') def should_reactivate_and_charge_subscription_when_valid_request(self, mock_request_with_token): mock_request_with_token.return_value = {"status": "reactivated"} - result = self.hotmart.reactivate_and_charge_subscription(subscriber_code=["123"], charge=True) + result = self.hotmart.reactivate_and_charge_subscription(subscriber_code=["123"], + charge=True) self.assertEqual(result, {"status": "reactivated"}) @patch.object(Hotmart, '_request_with_token') - def should_raise_exception_when_reactivating_and_charging_subscription_fails(self, mock_request_with_token): + def raise_exception_when_reactivating_and_charging_subscription_fails(self, + mock_request_with_token): mock_request_with_token.side_effect = RequestException("Error", "url") with self.assertRaises(RequestException): self.hotmart.reactivate_and_charge_subscription(subscriber_code=["123"], charge=True) From ca98fff78697d9c219636f63202548820a4a313e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matheus=20Ten=C3=B3rio?= Date: Thu, 21 Mar 2024 14:23:23 -0300 Subject: [PATCH 2/2] 0.3.0 --- .github/workflows/checks.yml | 39 ++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/checks.yml diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..4ed61e0 --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,39 @@ +--- +name: Checks + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + test-lint: + name: Test and Lint + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.8.1 + architecture: x64 + + - name: Install Poetry + uses: snok/install-poetry@v1.3.4 + with: + virtualenvs-create: true + virtualenvs-in-project: true + + - name: Install dependencies + run: poetry install --groups dev --no-root + + - name: Run tests + run: poetry run python -m unittest discover -s tests + + - name: Run flake8 + run: poetry run flake8