diff --git a/.gitignore b/.gitignore index 988e23d..e7e0bb4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ .idea */.egg-info/ dist +/DEVNOTES.txt diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index dc28a21..a035694 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,3 +1,25 @@ +0.5.0 / 2024-03-24 +================== + +* Changed underlying way of making requests, now standardizing the output of the response for it + to be always a list of dicts. +* Added new _handle_response method for standardizing the response. +* Added new decorator `@paginate` for handling pagination in the endpoints. +* Removed old `_paginate` method. +* Changed references to the old `_paginate` method to `_request_with_token`. +* Enhanced type hints for better readability. +* Most tests were refactored to reflect the changes in the new request handling. +* Fixed a bug where `subscriber_code` was not being passed to the request body as expected in + `change_due_day` method. +* Updated README with the new changes. + +0.4.1 / 2024-03-22 +================== + +* Better error handling for _make_request. +* Removed custom exceptions. +* Changed tests to better fit exceptions changes. + 0.4.0 / 2024-03-21 ================== diff --git a/docs/README-ptBR.md b/docs/README-ptBR.md index 8192f66..4f7f8b9 100644 --- a/docs/README-ptBR.md +++ b/docs/README-ptBR.md @@ -1,6 +1,7 @@ # Hotmart Python -Esse é um Wrapper desenvolvido em Python para a API da Hotmart que permite interagir com os recursos oferecidos pela API +Esse é um Wrapper desenvolvido em Python para a API da Hotmart que permite interagir com os recursos +oferecidos pela API Oficial da plataforma.: **Note**: The english docs is available [here](README.md). @@ -10,6 +11,11 @@ Oficial da plataforma.: - [Funcionalidades](#funcionalidades) - [Instalação](#instalção) - [Uso](#uso) + - [Exemplo de uso 1](#exemplo-de-uso-1) + - [Logs](#logs) + - [Sandbox](#sandbox) + - [Exemplo de uso 2](#exemplo-de-uso-2) + - [Paginação](#paginação) - [Parâmetros suportados](#parâmetros-suportados) - [Referência da API](#referência-da-api) - [Contribuição](#contribuição) @@ -18,7 +24,7 @@ Oficial da plataforma.: ## Funcionalidades: - ✅ Autenticação -- ✅ Todos os parâmetros de URL são suportados +- ✅ Paginação - ✅ Todos os endpoints de vendas - ✅ Todos os endpoints de assinaturas - ✅ Todos os endpoints de cupons @@ -31,6 +37,8 @@ pip install hotmart_python ## Uso +### Exemplo de uso 1 + Abaixo está um exmeplo de como usar a biblioteca Hotmart Python em seu código: ```python @@ -46,16 +54,20 @@ sales_history = hotmart.get_sales_history() print(sales_history) ``` +### Logs + Por padrão, os logs são desabilitados. Você pode ativá-los e configurar o nível dos logs passando o parâmetro `log_level` quando inicializar a classe Hotmart. Os níveis de logs disponíveis são: -- ️️☣️ `logging.DEBUG`: Logs para debug, que inclui informações detalhadas como URLs de solicitações (requests), - parâmetros de URL e itens do body (**não recomendado para uso em produção devido a informações sensíveis serem - logadas**). -- `logging.INFO`: Logs de informação, permitem a visualização de informações simples sobre as configurações da classe. +- ️️☣️ `logging.DEBUG`: Logs para debug, que inclui informações detalhadas como URLs de + solicitações (requests), parâmetros de URL e itens do body + (**não recomendado para uso em produção devido a informações sensíveis serem logadas**). +- `logging.INFO`: Logs de informação, permitem a visualização de informações simples sobre as + configurações da classe. - `logging.WARNING`: Logs de aviso, indicam potenciais problemas ou comportamentos inesperados. - `logging.ERROR`: Logs de erro, indicam quando erros ocorrem durante a interação com a API. -- `logging.CRITICAL`: Logs críticos, indicam erros críticos que podem impedir o funcionamento esperado. +- `logging.CRITICAL`: Logs críticos, indicam erros críticos que podem impedir o funcionamento + esperado. ```python import logging @@ -68,8 +80,13 @@ hotmart = Hotmart(client_id='your_client_id', log_level=logging.INFO) ``` -Você também pode usar o parâmetro `sandbox` para ativar o modo Sandbox, que deve ser criado préviamente usando as -credenciais Hotmart. Por padrão, o modo sandbox é desabilitado. +O parâmetro log_level pode ser omitido, se feito, irá voltar ao `log_level` padrão, que +é `logging.WARNING`. + +### Sandbox + +Também é possível usar o parâmetro `sandbox` para ativar o modo Sandbox, que deve ser criado +préviamente usando as credenciais Hotmart. Por padrão, o modo sandbox é desabilitado. ```python import logging @@ -83,8 +100,10 @@ hotmart = Hotmart(client_id='your_sandbox_client_id', sandbox=True) ``` -Exemplo de uso: Obter histórico de vendas com logs ativados e configurados para nível "INFO", filtrando por e-mail do -comprador: +### Exemplo de uso 2: + +Obter histórico de vendas com logs ativados e configurados para nível "INFO", filtrando por e-mail +do comprador: ```python from hotmart_python import Hotmart @@ -101,34 +120,83 @@ sales_history = hotmart.get_sales_history(buyer_email='johndoe@example.com') print(sales_history) ``` +### Paginação: + +Por padrão, a paginação é desabilitada. Se você quiser habilitá-la, pode usar o decorador `paginate` +há dois métodos recomendados para fazê-lo. + +O primeiro (e mais simples) é usar o decorator em uma função "wrapper", que irá apenas envolver e +passar os argumentos e argumentos de palavra para a função. + +```python +from hotmart_python import Hotmart +from hotmart_python.decorators import paginate + +hotmart = Hotmart(client_id='your_client_id', + client_secret='your_client_secret', + basic='your_basic') + + +@paginate +def historico_de_vendas(*args, **kwargs): + return hotmart.get_sales_history(*args, **kwargs) + + +print(historico_de_vendas()) +``` + +O segundo é uma função lambda de uma linha. + +```python +from hotmart_python import Hotmart +from hotmart_python.decorators import paginate + +hotmart = Hotmart(client_id='your_client_id', + client_secret='your_client_secret', + basic='your_basic') + +historico_de_vendas = paginate(lambda *args, **kwargs: hotmart.get_sales_history(*args, **kwargs)) +print(historico_de_vendas()) +``` + +O output da paginação é uma lista de dicionários, cada dicionário sendo um item dentro chave "items" +do retorno da chamada da API. + ## Parâmetros Suportados -Pelo formato de desenvolvimento da biblioteca, todos os parâmetros (tanto de URL quando de body) devem ser suportados +Pelo formato de desenvolvimento da biblioteca, todos os parâmetros (tanto de URL quando de body) +devem ser suportados por padrão, eles deverão ser passados como argumentos de palavra-chave (`**kwargs`) para os métodos. Esses são alguns dos parâmetros suportados pela classe `Hotmart`: -- `paginate` (`bool`): Se deve paginar os resultados ou não (o padrão é `False`). Quando definido como `True`, o método - irá buscar todas as páginas de dados para um endpoint paginado. -- `kwargs`: Quaisquer consultas suportadas pelo endpoint. Por exemplo, o método `get_sales_history` suporta os seguintes +- `kwargs`: Quaisquer consultas suportadas pelo endpoint. Por exemplo, o método `get_sales_history` + suporta os seguintes parâmetros: - `max_results` (`int`): O número máximo de itens por página que podem ser retornados. - `product_id` (`int`): Identificador único (ID) do produto vendido (número de 7 dígitos). - - `start_date` (`int`): Data de início do período de filtragem. A data deve estar em milissegundos, começando em + - `start_date` (`int`): Data de início do período de filtragem. A data deve estar em + milissegundos, começando em 01/01/1970 00:00:00 UTC. - - `end_date` (`int`): Data final do período de filtro. A data deve estar em milissegundos, começando em 01-01-1970 + - `end_date` (`int`): Data final do período de filtro. A data deve estar em milissegundos, + começando em 01-01-1970 00:00:00 UTC. - - `sales_source` (`str`): Código SRC utilizado no link da página de pagamento do produto para rastreamento da + - `sales_source` (`str`): Código SRC utilizado no link da página de pagamento do produto para + rastreamento da origem. ( Por exemplo: `pay.hotmart.com/B00000000T?src=campaignname`) - `buyer_name` (`str`): Nome do comprador. - - `buyer_email` (`str`): Endereço de e-mail do comprador. Você pode usar essas informações para pesquisar compras + - `buyer_email` (`str`): Endereço de e-mail do comprador. Você pode usar essas informações para + pesquisar compras específicas. - `product_id` (`str`): O ID do produto. - - `transaction` (`str`): Código de referência exclusivo para uma transação, por exemplo, HP17715690036014. Uma - transação acontece quando um pedido é feito. Um pedido pode ser um boleto bancário gerado, uma compra aprovada, um + - `transaction` (`str`): Código de referência exclusivo para uma transação, por exemplo, + HP17715690036014. Uma + transação acontece quando um pedido é feito. Um pedido pode ser um boleto bancário gerado, uma + compra aprovada, um pagamento recorrente e mais. - - `transaction_status` (`str`): O status da compra (Por exemplo: 'approved', 'pending', 'refunded', 'canceled', ' + - `transaction_status` (`str`): O status da compra (Por exemplo: 'approved', 'pending', ' + refunded', 'canceled', ' chargeback'). - E outros. @@ -139,44 +207,58 @@ a [documentação da API da Hotmart](https://developers.hotmart.com/docs/en/). Aqui está uma breve visão geral dos métodos suportados pela classe `Hotmart`: -- `get_sales_history(**kwargs)`: Recupera o histórico de vendas. Aceita argumentos de palavras-chave opcionais para - filtrar o resultados. [Referência](https://developers.hotmart.com/docs/pt-BR/v1/sales/sales-history/) +- `get_sales_history(**kwargs)`: Recupera o histórico de vendas. Aceita argumentos de palavras-chave + opcionais para filtrar o + esultados. [Referência](https://developers.hotmart.com/docs/pt-BR/v1/sales/sales-history/) -- `get_sales_summary(**kwargs)`: Recupera o resumo de vendas. Aceita argumentos de palavras-chave opcionais para filtrar +- `get_sales_summary(**kwargs)`: Recupera o resumo de vendas. Aceita argumentos de palavras-chave + opcionais para filtrar os resultados. [Referência](https://developers.hotmart.com/docs/pt-BR/v1/sales/sales-summary/) -- `get_sales_participants(**kwargs)`: Recupera os participantes de vendas. Aceita argumentos de palavras-chave opcionais - para filtrar os resultados. [Referência](https://developers.hotmart.com/docs/pt-BR/v1/sales/sales-users/) +- `get_sales_participants(**kwargs)`: Recupera os participantes de vendas. Aceita argumentos de + palavras-chave opcionais + para filtrar os + resultados. [Referência](https://developers.hotmart.com/docs/pt-BR/v1/sales/sales-users/) -- `get_sales_commissions(**kwargs)`: Recupera as comissões de vendas. Aceita argumentos de palavras-chave opcionais para - filtrar os resultados. [Referência](https://developers.hotmart.com/docs/pt-BR/v1/sales/sales-commissions/) +- `get_sales_commissions(**kwargs)`: Recupera as comissões de vendas. Aceita argumentos de + palavras-chave opcionais para + filtrar os + resultados. [Referência](https://developers.hotmart.com/docs/pt-BR/v1/sales/sales-commissions/) -- `get_sales_price_details(**kwargs)`: Recupera os detalhes do preço de venda. Aceita argumentos de palavras-chave +- `get_sales_price_details(**kwargs)`: Recupera os detalhes do preço de venda. Aceita argumentos de + palavras-chave opcionais para filtrar os resultados. [Referência](https://developers.hotmart.com/docs/pt-BR/v1/sales/sales-price-details/) -- `get_subscriptions(paginate=False, **kwargs)`: Recupera as assinaturas. Aceita um argumento opcional `paginate` e +- `get_subscriptions(paginate=False, **kwargs)`: Recupera as assinaturas. Aceita um argumento + opcional `paginate` e argumentos adicionais de palavras-chave para filtrar os resultados. [Referência](https://developers.hotmart.com/docs/pt-BR/v1/subscription/get-subscribers/) -- `get_subscription_summary(paginate=False, **kwargs)`: Recupera o sumário da assinatura. Aceita um argumento +- `get_subscription_summary(paginate=False, **kwargs)`: Recupera o sumário da assinatura. Aceita um + argumento opcional `paginate` e argumentos de palavras-chave adicionais para filtrar os resultados. [Referência](https://developers.hotmart.com/docs/pt-BR/v1/subscription/get-subscription-summary/) -- `get_subscription_purchases(subscriber_code, paginate=False, **kwargs)`: Recupera as compras de assinatura para um - assinante específico. Requer um argumento `subscriber_code` e aceita um argumento opcional `paginate` argumentos de +- `get_subscription_purchases(subscriber_code, paginate=False, **kwargs)`: Recupera as compras de + assinatura para um + assinante específico. Requer um argumento `subscriber_code` e aceita um argumento + opcional `paginate` argumentos de palavra-chave adicionais para filtrar os resultados. [Referência](https://developers.hotmart.com/docs/pt-BR/v1/subscription/get-subscription-purchases/) -- `cancel_subscription(subscriber_code, send_email=True)`: Cancela uma assinatura. Requer um argumento `subscriber_code` +- `cancel_subscription(subscriber_code, send_email=True)`: Cancela uma assinatura. Requer um + argumento `subscriber_code` e aceita um `send_email` como argumento opcional. [Referência](https://developers.hotmart.com/docs/pt-BR/v1/subscription/cancel-subscriptions/) -- `reactivate_and_charge_subscription(subscriber_code, charge=True)`: Reativa e cobra uma assinatura. Requer um +- `reactivate_and_charge_subscription(subscriber_code, charge=True)`: Reativa e cobra uma + assinatura. Requer um argumento `subscriber_code` e aceita um argumento opcional `charge`. [Referência](https://developers.hotmart.com/docs/pt-BR/v1/subscription/reactivate-subscription/) -Para uma informação mais detalhada sobre os endpoints aos quais esses métodos se referem e os parâmetros aceitos, por +Para uma informação mais detalhada sobre os endpoints aos quais esses métodos se referem e os +parâmetros aceitos, por favor, visite [documentação oficial da API da Hotmart](https://developers.hotmart.com/docs/pt-BR/). @@ -187,7 +269,9 @@ o [guia de contribuição](CONTRIBUTING.md) (disponível somente em inglês) par ## Licença -This project is licensed under the Apache License 2.0 - see the [LICENSE](../LICENSE.txt) file for details. +This project is licensed under the Apache License 2.0 - see the [LICENSE](../LICENSE.txt) file for +details. -Essa biblioteca não tem filiação com a Hotmart. É um projeto de código aberto que não é suportado oficialmente pela +Essa biblioteca não tem filiação com a Hotmart. É um projeto de código aberto que não é suportado +oficialmente pela Hotmart. diff --git a/docs/README.md b/docs/README.md index b19ab23..aa78f67 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,6 +10,11 @@ the platform: - [Features](#features) - [Installation](#installation) - [Usage](#usage) + - [Usage example 1](#usage-example-1) + - [Logs](#logs) + - [Sandbox](#sandbox) + - [Usage example 2](#usage-example-2) + - [Pagination](#pagination) - [Supported Parameters](#supported-parameters) - [API Reference](#api-reference) - [Contributing](#contributing) @@ -18,6 +23,7 @@ the platform: ## Features: - ✅ Authentication +- ✅ Pagination - ✅ All sales endpoints - ✅ All subscriptions endpoints - ✅ All coupons endpoints @@ -30,6 +36,8 @@ pip install hotmart-python ## Usage +### Usage example 1: + Here's how you can use the Hotmart Python Wrapper in your Python code: ```python @@ -45,13 +53,14 @@ sales_history = hotmart.get_sales_history() print(sales_history) ``` +### Logs: + By default, logging is disabled. You can enable it and set the log level by passing the `log_level` -parameter when -initializing the Hotmart object. The available log levels are: +parameter when initializing the Hotmart object. The available log levels are: - ️️☣️ `logging.DEBUG`: Debug level logging, which includes detailed information such as request - URLs and parameters (* - *not recommended for production use due to sensitive information being logged**). + URLs and parameters + (**not recommended for production use due to sensitive information being logged**). - `logging.INFO`: Information level logging, which provides basic information about the operations being performed. - `logging.WARNING`: Warning level logging, which indicates potential issues or unexpected behavior. @@ -70,9 +79,14 @@ hotmart = Hotmart(client_id='your_client_id', log_level=logging.INFO) ``` -You can also use the `sandbox` parameter to enable the sandbox environment. By default, the sandbox -environment is -disabled. +The parameter log_level can be omitted, and if done, it will fall back to the default log level, +which is `logging.WARNING`. + +### Sandbox: + +It is also possible to use the `sandbox` parameter to enable the sandbox environment, which can only +be accessed if previously generated credentials targeting sandbox mode are generated inside Hotmart. +By default, e sandbox environment is disabled. ```python import logging @@ -86,7 +100,10 @@ hotmart = Hotmart(client_id='your_sandbox_client_id', sandbox=True) ``` -Usage example for getting sales history with logging enabled and log level set to INFO: +### Usage example 2: + +Usage example for getting sales history with logging enabled and log level set to INFO and using +query filters to get sales by buyer email: ```python from hotmart_python import Hotmart @@ -103,13 +120,48 @@ sales_history = hotmart.get_sales_history(buyer_email='johndoe@example.com') print(sales_history) ``` +### Pagination: + +By default, pagination is disabled. If you want to enable pagination, you can use de `paginate` +decorator, there are two ways of doing it. + +The first one (and simplest) is to use the decorator in a wrapper function + +```python +from hotmart_python import Hotmart +from hotmart_python.decorators import paginate + +hotmart = Hotmart(client_id='your_client_id', + client_secret='your_client_secret', + basic='your_basic') + + +@paginate +def get_sales_history(*args, **kwargs): + return hotmart.get_sales_history(*args, **kwargs) + + +print(get_sales_history()) +``` + +The second one, it's a one-liner lambda function + +```python +from hotmart_python import Hotmart +from hotmart_python.decorators import paginate + +hotmart = Hotmart(client_id='your_client_id', + client_secret='your_client_secret', + basic='your_basic') + +get_sales_history = paginate(lambda *args, **kwargs: hotmart.get_sales_history(*args, **kwargs)) +print(get_sales_history()) +``` + ## Supported Parameters These are the supported parameters for all methods that interact with the Hotmart API: -- `paginate` (bool): Whether to paginate the results or not (default is False). When set to True, - the method will fetch - all pages of data for a paginated endpoint. - `kwargs`: Any queries that are supported by the endpoint. For example, the `get_sales_history` method supports the following parameters: diff --git a/hotmart_python/__init__.py b/hotmart_python/__init__.py index 0ef31cf..f6a1905 100644 --- a/hotmart_python/__init__.py +++ b/hotmart_python/__init__.py @@ -1 +1,2 @@ -from .hotmart import Hotmart, HTTPRequestException, RequestException +from .hotmart import Hotmart +from .decorators import paginate diff --git a/hotmart_python/decorators.py b/hotmart_python/decorators.py new file mode 100644 index 0000000..837828f --- /dev/null +++ b/hotmart_python/decorators.py @@ -0,0 +1,57 @@ +import sys +import logging +import coloredlogs +from typing import Callable, Dict, Any, List + +# Base Logging Configs +logger = logging.getLogger(__name__) # noqa + +# Coloredlogs Configs +coloredFormatter = coloredlogs.ColoredFormatter( + fmt='[%(name)s] %(asctime)s %(message)s', + level_styles=dict( + debug=dict(color='white'), + info=dict(color='blue'), + warning=dict(color='yellow', bright=True), + error=dict(color='red', bold=True, bright=True), + critical=dict(color='black', bold=True, background='red'), + ), + field_styles=dict( + name=dict(color='white'), + asctime=dict(color='white'), + funcName=dict(color='white'), + lineno=dict(color='white'), + ) +) + +# Console Handler Configs +ch = logging.StreamHandler(stream=sys.stdout) +ch.setFormatter(fmt=coloredFormatter) +logger.addHandler(hdlr=ch) +logger.setLevel(level=logging.CRITICAL) + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + + +def paginate(func: Callable[..., List[Dict[str, Any]]]) -> Callable[..., List[Dict[str, Any]]]: + def wrapper(*args: Any, **kwargs: Any) -> List[Dict[str, Any]]: + items = [] + response: List = func(*args, **kwargs, enhance=False) + + try: + items.extend(response[0]["items"]) + + while 'page_info' in response[0] and 'next_page_token' in response[0]["page_info"]: + logger.debug(f"Next page token: {response[0]['page_info']['next_page_token']}") + kwargs['page_token'] = response[0]['page_info']['next_page_token'] + response = func(*args, **kwargs, enhance=False) + for obj in response: + if "items" in obj: + items.extend(obj["items"]) + except KeyError: + logger.debug("KeyError") + return response + logger.debug("Finished fetching all pages.") + return items + return wrapper diff --git a/hotmart_python/hotmart.py b/hotmart_python/hotmart.py index 93819f5..761a073 100644 --- a/hotmart_python/hotmart.py +++ b/hotmart_python/hotmart.py @@ -6,6 +6,8 @@ from typing import List, Dict, Any, Optional +Response = List[Dict[str, Any]] + # Base Logging Configs logger = logging.getLogger(__name__) @@ -34,33 +36,6 @@ logger.setLevel(level=logging.CRITICAL) -class HTTPRequestException(Exception): - def __init__(self, message, status_code, url, response_body=None): - super().__init__(message) - self.status_code = status_code - self.url = url - self.response_body = response_body - - def __str__(self): - return (f"HTTPRequestException: {self.args[0]}, " - f"Status Code: {self.status_code}, " - f"URL: {self.url}, Response Body: {self.response_body}") - - -class RequestException(Exception): - def __init__(self, message, url): - super().__init__(message) - self.url = url - - def __str__(self): - return f"RequestException: {self.args[0]}, URL: {self.url}" - - -# URL Configs -PRODUCTION_BASE_URL = "https://developers.hotmart.com/payments/api/" -SANDBOX_BASE_URL = "https://sandbox.hotmart.com/payments/api/" - - class Hotmart: def __init__(self, client_id: str, client_secret: str, basic: str, api_version: int = 1, @@ -81,8 +56,7 @@ def __init__(self, client_id: str, client_secret: str, basic: str, self.secret = client_secret self.basic = basic self.sandbox = sandbox - self.base_url = SANDBOX_BASE_URL if sandbox else PRODUCTION_BASE_URL - self.base_url = f'{self.base_url}v{api_version}' + self.api_version = api_version # Token caching and some logic to do better logging. self.token_cache = None @@ -92,12 +66,6 @@ def __init__(self, client_id: str, client_secret: str, basic: str, self.logger = logging.getLogger(__name__) self.logger.setLevel(log_level) - def _sandbox_error_warning(self): - self.logger.warning("At the date of last update for this library" - " the Hotmart Sandbox API does NOT supported this method.") - self.logger.warning("This method probably won't work in the Sandbox mode.") - return - @staticmethod def _build_payload(**kwargs: Any) -> Dict[str, Any]: """ @@ -113,6 +81,61 @@ def _build_payload(**kwargs: Any) -> Dict[str, Any]: payload[key] = value return payload + @staticmethod + def _handle_response(response: Dict[str, Any] | List[Dict[str, Any]], + enhance=True) -> Response: + """ + Standardizes the output of the response to always be a list of dictionaries. + :param response: The original response which can be a dictionary or a list of dictionaries. + :param enhance: When True, discards page_info and returns only the items. (Default is True) + :return: A list of dictionaries. + """ + try: + if enhance: + if isinstance(response, dict) and response["items"]: + return response["items"] + + if isinstance(response, dict) and response["lessons"]: + return response["lessons"] + + if isinstance(response, list): + return response + + if not enhance: + if isinstance(response, dict): + return [response] + + if isinstance(response, list): + return response + + raise ValueError("Response must be a dictionary or a list of dictionaries.") + except KeyError: + return [response] + + def _build_url(self, endpoint_type: str): + """ + Builds the URLs for better dynamic requests. + :param endpoint_type: Supported types: payments or club + :return: + """ + + supported_types = ['payments', 'club'] + if endpoint_type.lower() not in supported_types: + raise ValueError(f"Unsupported endpoint type: {endpoint_type}") + + return (f'https://{"sandbox" if self.sandbox else "developers"}.hotmart.com/' + f'{endpoint_type.lower()}/api/v{self.api_version}') + + def _sandbox_error_warning(self): + """ + Logs a warning message about the Hotmart Sandbox API not supporting some requests + :return: + """ + self.logger.warning("At the date of last update for this library" + " the Hotmart Sandbox API does NOT supported this method.") + self.logger.warning("This method probably won't work in the Sandbox mode.") + return + def _log_instance_mode(self) -> None: """ Logs the instance mode (Sandbox or Production). @@ -121,11 +144,13 @@ def _log_instance_mode(self) -> None: return self.logger.warning( f"Instance in {'Sandbox' if self.sandbox else 'Production'} mode") - def _make_request(self, method: Any, url: str, + 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]]: + log_level: Optional[int] = None) -> Dict[str, Any] | List[Dict[str, Any]]: """ Makes a request to the given url. :param method: The request method (e.g, requests.get, requests.post). @@ -141,54 +166,49 @@ def _make_request(self, method: Any, url: str, if log_level is not None: logger = logging.getLogger(__name__) # noqa logger.setLevel(log_level) - try: - self.logger.debug(f"Request URL: {url}") - self.logger.debug(f"Request headers: {headers}") - self.logger.debug(f"Request params: {params}") - self.logger.debug(f"Request body: {body}") + self.logger.debug(f"Request URL: {url}") + self.logger.debug(f"Request headers: {headers}") + self.logger.debug(f"Request params: {params}") + self.logger.debug(f"Request body: {body}") + + try: response = method(url, headers=headers, params=params, data=body) self.logger.debug(f"Response content: {response.text}") + if response.status_code == requests.codes.ok: + return response.json() response.raise_for_status() - return response.json() - except requests.exceptions.HTTPError: + except requests.exceptions.HTTPError as e: # noinspection PyUnboundLocalVariable - if response.status_code != 403: - 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( - "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) + if e.response.status_code == 401 or e.response.status_code == 403: + if self.sandbox: + self.logger.error("Perhaps the credentials aren't for Sandbox Mode?") + else: + self.logger.error("Perhaps the credentials are for Sandbox Mode?") + raise e + + if e.response.status_code == 422: + self.logger.error(f"Error {e.response.status_code}") + self.logger.error("This usually happens when the request is missing" + " body parameters.") + raise e + + if e.response.status_code == 500 and self.sandbox: + self.logger.error("This happens with some endpoints in the Sandbox Mode.") + self.logger.error("Usually the API it's not down, it's just a bug.") + + raise e 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.") + self.logger.debug("Fetching a new access token.") method_url = 'https://api-sec-vlc.hotmart.com/security/oauth/token' headers = {'Authorization': self.basic} @@ -197,21 +217,21 @@ def _fetch_new_token(self) -> str: response = self._make_request(requests.post, method_url, headers=headers, params=payload) - self.logger.info("Token obtained successfully") + self.logger.debug("Token obtained successfully") return response['access_token'] - def _get_token(self) -> Optional[str]: + def _get_token(self) -> str: """ Retrieves an access token to authenticate requests. :return: The access token if obtained successfully, otherwise None. """ if not self._is_token_expired() and self.token_cache is not None: if not self.token_found_in_cache: - self.logger.info("Token found in cache.") + self.logger.debug("Token found in cache.") self.token_found_in_cache = True return self.token_cache - self.logger.info("Token not found in cache or expired.") + self.logger.debug("Token not found in cache or expired.") token = self._fetch_new_token() if token is not None: @@ -219,8 +239,9 @@ def _get_token(self) -> Optional[str]: self.token_found_in_cache = False return token - def _request_with_token(self, method: str, url: str, body: Optional[Dict[str, Any]] = None, - params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + def _request_with_token(self, method: str, url: str, enhance: bool = None, + body: Optional[Dict[str, Any]] = None, + params: Optional[Dict[str, Any]] = None) -> Response: """ Makes an authenticated request (GET, POST, PATCH, etc.) to the specified URL with the given body or params. @@ -248,181 +269,139 @@ 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) - - 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. - """ - if not paginate: - response = self._request_with_token(method=method, url=url, params=params) - return response.get("items", []) if response else None - all_items = [] - - self.logger.info("Fetching first page...") - response = self._request_with_token(method=method, url=url, params=params) - - if response is None: - raise ValueError("Failed to fetch first page.") + response = self._make_request(method_mapping[method.upper()], url, headers=headers, + params=params, body=body) - all_items.extend(response.get("items", [])) + return self._handle_response(response, enhance=enhance) - while "next_page_token" in response.get("page_info", {}): - next_page_token = response["page_info"]["next_page_token"] - self.logger.info(f"Fetching next page with token: {next_page_token}") - params["page_token"] = next_page_token - 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}") - - all_items.extend(response.get("items", [])) - - self.logger.info("Finished fetching all pages.") - return all_items - - def get_sales_history(self, paginate: bool = False, **kwargs: Any) -> Optional[Dict[str, Any]]: + def get_sales_history(self, enhance: bool = True, **kwargs: Any) -> Response: """ Retrieves sales history data based on the provided filters. - + :param enhance: When True, discards page_info and returns only the items. (Default is True) :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. """ + self._log_instance_mode() method = "get" - url = f'{self.base_url}/sales/history' + base_url = self._build_url('payments') + url = f'{base_url}/sales/history' payload = self._build_payload(**kwargs) - return self._pagination(method=method, url=url, params=payload, - paginate=paginate) + return self._request_with_token(method=method, url=url, params=payload, enhance=enhance) - def get_sales_summary(self, paginate: bool = False, **kwargs: Any) -> Optional[Dict[str, Any]]: + def get_sales_summary(self, enhance: bool = True, **kwargs: Any) -> Response: """ Retrieves sales summary data based on the provided filters. - + :param enhance: When True, discards page_info and returns only the items. (Default is True) :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. """ self._log_instance_mode() method = "get" - url = f'{self.base_url}/sales/summary' + base_url = self._build_url('payments') + url = f'{base_url}/sales/summary' payload = self._build_payload(**kwargs) - return self._pagination(method=method, url=url, params=payload, - paginate=paginate) + return self._request_with_token(method=method, url=url, params=payload, enhance=enhance) - def get_sales_participants(self, paginate: bool = True, **kwargs: Any) -> \ - Optional[Dict[str, Any]]: + def get_sales_participants(self, enhance: bool = True, **kwargs: Any) -> Response: """ Retrieves sales user data based on the provided filters. - + :param enhance: When True, discards page_info and returns only the items. (Default is True) :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. """ self._log_instance_mode() method = "get" - url = f'{self.base_url}/sales/users' + base_url = self._build_url('payments') + url = f'{base_url}/sales/users' payload = self._build_payload(**kwargs) - return self._pagination(method=method, url=url, params=payload, paginate=paginate) + return self._request_with_token(method=method, url=url, params=payload, enhance=enhance) - def get_sales_commissions(self, paginate: bool = False, **kwargs: Any) -> \ - Optional[Dict[str, Any]]: + def get_sales_commissions(self, enhance: bool = True, **kwargs: Any) -> Response: """ Retrieves sales commissions data based on the provided filters. - + :param enhance: When True, discards page_info and returns only the items. (Default is True) :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. """ self._log_instance_mode() method = "get" - url = f'{self.base_url}/sales/commissions' + base_url = self._build_url('payments') + url = f'{base_url}/sales/commissions' payload = self._build_payload(**kwargs) - return self._pagination(method=method, url=url, params=payload, paginate=paginate) + return self._request_with_token(method=method, url=url, params=payload, enhance=enhance) - def get_sales_price_details(self, paginate: bool = False, **kwargs: Any) \ - -> Optional[Dict[str, Any]]: + def get_sales_price_details(self, enhance: bool = True, **kwargs: Any) -> Response: """ Retrieves sales price details based on the provided filters. - + :param enhance: When True, discards page_info and returns only the items. (Default is True) :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. """ self._log_instance_mode() method = "get" - url = f'{self.base_url}/sales/price/details' + base_url = self._build_url('payments') + url = f'{base_url}/sales/price/details' payload = self._build_payload(**kwargs) - return self._pagination(method=method, url=url, params=payload, paginate=paginate) + return self._request_with_token(method=method, url=url, params=payload, enhance=enhance) - def get_subscriptions(self, paginate: bool = False, **kwargs: Any) -> \ - Optional[Dict[str, Any]]: + def get_subscriptions(self, enhance: bool = True, **kwargs: Any) -> Response: """ Retrieves subscription data based on the provided filters. - + :param enhance: When True, discards page_info and returns only the items. (Default is True) :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. """ self._log_instance_mode() method = "get" - url = f'{self.base_url}/subscriptions' + base_url = self._build_url('payments') + url = f'{base_url}/subscriptions' payload = self._build_payload(**kwargs) - return self._pagination(method=method, url=url, params=payload, paginate=paginate) + return self._request_with_token(method=method, url=url, params=payload, enhance=enhance) - def get_subscriptions_summary(self, paginate: bool = False, **kwargs: Any) -> \ - Optional[Dict[str, Any]]: + def get_subscriptions_summary(self, enhance: bool = True, **kwargs: Any) -> Response: """ Retrieves subscription summary data based on the provided filters. - :param paginate: Whether to paginate the results or not (default is False). + :param enhance: When True, discards page_info and returns only the items. (Default is True) :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. """ self._log_instance_mode() + self._sandbox_error_warning() method = "get" - url = f'{self.base_url}/subscriptions/summary' + base_url = self._build_url('payments') + url = f'{base_url}/subscriptions/summary' payload = self._build_payload(**kwargs) - return self._pagination(method=method, url=url, params=payload, paginate=paginate) + return self._request_with_token(method=method, url=url, params=payload, enhance=enhance) - def get_subscription_purchases(self, subscriber_code, paginate: bool = False, **kwargs: Any) ->\ - Optional[Dict[str, Any]]: + def get_subscription_purchases(self, subscriber_code, enhance: bool = True, **kwargs: Any) -> \ + Response: """ Retrieves subscription purchases data based on the provided filters. - + :param enhance: When True, discards page_info and returns only the items. (Default is True) :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 paginate: Whether to paginate the results or not (default is False). :return: Subscription purchases data if available, otherwise None. """ @@ -431,12 +410,12 @@ def get_subscription_purchases(self, subscriber_code, paginate: bool = False, ** self._sandbox_error_warning() method = "get" - url = f'{self.base_url}/subscriptions/{subscriber_code}/purchases' + base_url = self._build_url('payments') + url = f'{base_url}/subscriptions/{subscriber_code}/purchases' payload = self._build_payload(**kwargs) - return self._pagination(method=method, url=url, params=payload, paginate=paginate) + return self._request_with_token(method=method, url=url, params=payload, enhance=enhance) - 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) -> Response: """ Cancels a subscription. @@ -450,15 +429,16 @@ def cancel_subscription(self, subscriber_code: list[str], send_email: bool = Tru self._sandbox_error_warning() method = "post" - url = f'{self.base_url}/subscriptions/cancel' + base_url = self._build_url('payments') + url = f'{base_url}/subscriptions/cancel' payload = { "subscriber_code": subscriber_code, "send_email": send_email } 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) \ + -> Response: """ Reactivates and charges a subscription. @@ -473,14 +453,15 @@ def reactivate_and_charge_subscription(self, subscriber_code: list[str], charge: self._sandbox_error_warning() method = "post" - url = f'{self.base_url}/subscriptions/reactivate' + base_url = self._build_url('payments') + url = f'{base_url}/subscriptions/reactivate' payload = { "subscriber_code": subscriber_code, "charge": charge } return self._request_with_token(method=method, url=url, body=payload) - def change_due_day(self, subscriber_code: str, new_due_day: int) -> Optional[Dict[str, Any]]: + def change_due_day(self, subscriber_code: str, new_due_day: int) -> Response: """ Changes the due day of a subscription. @@ -494,18 +475,16 @@ def change_due_day(self, subscriber_code: str, new_due_day: int) -> Optional[Dic self._sandbox_error_warning() method = "patch" - url = f'{self.base_url}/subscriptions/change-due-day' + base_url = self._build_url('payments') + url = f'{base_url}/subscriptions/{subscriber_code}' payload = { - "subscriber_code": subscriber_code, - "new_due_day": new_due_day + "due_day": new_due_day } return self._request_with_token(method=method, url=url, body=payload) - def create_coupon(self, product_id: str, coupon_code: str, discount: float) -> \ - Optional[Dict]: + def create_coupon(self, product_id: str, coupon_code: str, discount: float) -> Response: """ Creates a coupon for a product. - :param product_id: UID of the product you want to create the coupon for. :param coupon_code: The code of the coupon you want to create. :param discount: The discount you want to apply to the coupon, must be greater than 0 and @@ -518,18 +497,20 @@ def create_coupon(self, product_id: str, coupon_code: str, discount: float) -> \ self._sandbox_error_warning() method = "post" - url = f'{self.base_url}/product/{product_id}/coupon' + base_url = self._build_url('payments') + url = f'{base_url}/product/{product_id}/coupon' payload = { "code": coupon_code, "discount": discount } return self._request_with_token(method=method, url=url, body=payload) - def get_coupon(self, product_id: str) -> Optional[Dict[str, Any]]: + def get_coupon(self, product_id: str, code: str, enhance: bool = True) -> Response: """ Retrieves a coupon for a product. - + :param enhance: When True, discards page_info and returns only the items. (Default is True) :param product_id: UID of the product you want to retrieve the coupon for. + :param code: The code of the coupon you want to retrieve. :return: All Coupons for the product. """ @@ -538,8 +519,12 @@ def get_coupon(self, product_id: str) -> Optional[Dict[str, Any]]: self._sandbox_error_warning() method = "get" - url = f'{self.base_url}/coupon/product/{product_id}' - return self._request_with_token(method=method, url=url) + base_url = self._build_url('payments') + url = f'{base_url}/coupon/product/{product_id}' + params = { + "code": code + } + return self._request_with_token(method=method, url=url, params=params, enhance=enhance) def delete_coupon(self, coupon_id): """ @@ -553,5 +538,6 @@ def delete_coupon(self, coupon_id): self._sandbox_error_warning() method = "delete" - url = f'{self.base_url}/coupon/{coupon_id}' + base_url = self._build_url('payments') + url = f'{base_url}/coupon/{coupon_id}' return self._request_with_token(method=method, url=url) diff --git a/pyproject.toml b/pyproject.toml index 7f6588d..1239aef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hotmart-python" -version = "0.4.0" +version = "0.5.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" diff --git a/tests/test_coupons.py b/tests/test_coupons.py index 0ce1e98..4318acd 100644 --- a/tests/test_coupons.py +++ b/tests/test_coupons.py @@ -1,106 +1,74 @@ import unittest from unittest.mock import patch -from hotmart_python import Hotmart, RequestException, HTTPRequestException +from requests import Response +from hotmart_python import Hotmart client_id = 'b32450c1-1352-246a-b6d3-d49d6db815ea' client_secret = '90bcc221-cebd-5a5b-00e2-72cab47d9282' -basic = ('Basic YjIzNTQxYzAtMyEzNS20MjVhLWI1ZDItZDM4ZDVkYjcwNGVhOjA5Y2JiMTEzLWRiZWMtNGI0YS05OWUxLT\ -I3Y2FiNDdkOTI4Mg==') +basic = ('Basic YjIzNTQxYzAtMyEzNS20MjVhLWI1ZDItZDM4ZDVkYjcwNGVhOjA5Y2JiMTEz' + 'LWRiZWMtNGI0YS05OWUxLTI3Y2FiNDdkOTI4Mg==') -class TestHotmart(unittest.TestCase): +class TestSales(unittest.TestCase): def setUp(self): self.hotmart = Hotmart(client_id=client_id, client_secret=client_secret, basic=basic) @patch.object(Hotmart, '_request_with_token') - def test_create_coupon_successfully(self, mock_request_with_token): - mock_request_with_token.return_value = {} - result = self.hotmart.create_coupon('product_id', - 'coupon_code', - 0.5) - - self.assertEqual(result, {}) - - @patch.object(Hotmart, '_request_with_token') - def test_create_coupon_with_invalid_discount(self, mock_request_with_token): - mock_request_with_token.side_effect = HTTPRequestException("HTTP Error", - 404, - "url") - with self.assertRaises(HTTPRequestException): - self.hotmart.create_coupon('product_id', - 'coupon_code', - 1.5) - - @patch.object(Hotmart, '_request_with_token') - def test_create_coupon_with_negative_discount(self, mock_request_with_token): - mock_request_with_token.side_effect = HTTPRequestException("HTTP Error", - 404, - "url") - with self.assertRaises(HTTPRequestException): - self.hotmart.create_coupon('product_id', 'coupon_code', -0.5) - - @patch.object(Hotmart, '_request_with_token') - def test_create_coupon_with_zero_discount(self, mock_request_with_token): - mock_request_with_token.side_effect = HTTPRequestException("HTTP Error", - 404, - "url") - with self.assertRaises(HTTPRequestException): - self.hotmart.create_coupon('product_id', 'coupon_code', 0) - - @patch.object(Hotmart, '_request_with_token') - def test_create_coupon_with_request_exception(self, mock_request_with_token): - mock_request_with_token.side_effect = RequestException("Error", "url") - - with self.assertRaises(RequestException): - self.hotmart.create_coupon('product_id', - 'coupon_code', - 0.5) - - @patch.object(Hotmart, '_request_with_token') - def test_get_coupon_successfully(self, mock_request_with_token): - - mock_request_with_token.return_value = {"coupon": "COUPON_CODE"} - result = self.hotmart.get_coupon('product_id') - self.assertEqual(result, {"coupon": "COUPON_CODE"}) - - @patch.object(Hotmart, '_request_with_token') - def test_get_coupon_with_invalid_product_id(self, mock_request_with_token): - - mock_request_with_token.side_effect = HTTPRequestException("HTTP Error", - 404, - "url") - with self.assertRaises(HTTPRequestException): - self.hotmart.get_coupon('invalid_product_id') - - @patch.object(Hotmart, '_request_with_token') - def test_get_coupon_with_request_exception(self, mock_request_with_token): - - mock_request_with_token.side_effect = RequestException("Error", - "url") - with self.assertRaises(RequestException): - self.hotmart.get_coupon('product_id') - - @patch.object(Hotmart, '_request_with_token') - def test_delete_coupon_successfully(self, mock_request_with_token): - mock_request_with_token.return_value = {} - - result = self.hotmart.delete_coupon('coupon_id') - self.assertEqual(result, {}) + def test_create_coupon(self, mock_req_with_token): + mock_response = Response() + mock_response.status_code = 200 + mock_req_with_token.return_value = mock_response + + product_id = 'HP20219' + coupon_code = 'testcoupon' + coupon_discount = 0.5 + self.hotmart.create_coupon(product_id, coupon_code, coupon_discount) + expected_url = f'https://developers.hotmart.com/payments/api/v1/product/{product_id}/coupon' + + mock_req_with_token.assert_called_once_with(method="post", + url=expected_url, + body={ + 'code': coupon_code, + 'discount': coupon_discount + }) @patch.object(Hotmart, '_request_with_token') - def test_delete_coupon_with_invalid_coupon_id(self, mock_request_with_token): - mock_request_with_token.side_effect = HTTPRequestException("HTTP Error", - 404, - "url") - - with self.assertRaises(HTTPRequestException): - self.hotmart.delete_coupon('invalid_coupon_id') + def test_get_coupon(self, mock_req_with_token): + mock_response = Response() + mock_response.status_code = 200 + mock_response.return_value = { + 'items': [{ + 'some': 'info' + }] + } + mock_req_with_token.return_value = mock_response + + product_id = 'HP20219' + coupon_code = 'testcoupon' + self.hotmart.get_coupon(product_id, coupon_code) + expected_url = f'https://developers.hotmart.com/payments/api/v1/coupon/product/{product_id}' + + mock_req_with_token.assert_called_once_with(method="get", + url=expected_url, + params={ + 'code': coupon_code + }, enhance=True) @patch.object(Hotmart, '_request_with_token') - def test_delete_coupon_with_request_exception(self, mock_request_with_token): - - mock_request_with_token.side_effect = RequestException("Error", "url") - with self.assertRaises(RequestException): - self.hotmart.delete_coupon('coupon_id') + def test_delete_coupon(self, mock_req_with_token): + mock_response = Response() + mock_response.status_code = 200 + mock_response.return_value = { + 'items': [{ + 'some': 'info' + }] + } + mock_req_with_token.return_value = mock_response + + coupon_id = '123456' + self.hotmart.delete_coupon(coupon_id) + expected_url = f'https://developers.hotmart.com/payments/api/v1/coupon/{coupon_id}' + + mock_req_with_token.assert_called_once_with(method="delete", url=expected_url) diff --git a/tests/test_decorators.py b/tests/test_decorators.py new file mode 100644 index 0000000..d4fc80d --- /dev/null +++ b/tests/test_decorators.py @@ -0,0 +1,52 @@ +import unittest + +from unittest.mock import Mock +from hotmart_python.decorators import paginate + +client_id = 'b32450c1-1352-246a-b6d3-d49d6db815ea' +client_secret = '90bcc221-cebd-5a5b-00e2-72cab47d9282' +basic = ('Basic YjIzNTQxYzAtMyEzNS20MjVhLWI1ZDItZDM4ZDVkYjcwNGVhOjA5Y2JiMTEz' + 'LWRiZWMtNGI0YS05OWUxLTI3Y2FiNDdkOTI4Mg==') + + +class TestPaginateDecorator(unittest.TestCase): + def setUp(self): + self.func = Mock(return_value=[{ + "items": [{"id": 1}, {"id": 2}], + "page_info": { + "next_page_token": "token" + } + }]) + self.paginated_func = paginate(self.func) + + def test_paginate_when_no_next_page_token(self): + self.func.return_value = [{'items': [{'id': 1}, {'id': 2}]}] + result = self.paginated_func() + self.assertEqual(result, [{'id': 1}, {'id': 2}]) + self.func.assert_called_once() + + def test_paginate_handles_empty_items(self): + self.func.return_value = [{'items': []}] + result = self.paginated_func() + self.assertEqual(result, []) + self.func.assert_called_once() + + def test_paginate_handles_no_items_key_in_response(self): + self.func.return_value = [{}] + result = self.paginated_func() + self.assertEqual(result, [{}]) + self.func.assert_called_once() + + def test_paginate_response_is_list(self): + self.func.return_value = [ + {"name": "Dripping 100 days", "page_order": 1}, + {"name": "Dripping BY_DATE", "page_order": 2}, + {"name": "Offer product", "page_order": 3} + ] + result = self.paginated_func() + self.assertEqual(result, [ + {"name": "Dripping 100 days", "page_order": 1}, + {"name": "Dripping BY_DATE", "page_order": 2}, + {"name": "Offer product", "page_order": 3} + ]) + self.func.assert_called_once() diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 0f84bf6..aa0eefb 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,7 +1,8 @@ import requests import unittest +from unittest.mock import Mock, MagicMock from unittest.mock import patch -from hotmart_python import Hotmart, RequestException, HTTPRequestException +from hotmart_python import Hotmart client_id = 'b32450c1-1352-246a-b6d3-d49d6db815ea' client_secret = '90bcc221-cebd-5a5b-00e2-72cab47d9282' @@ -9,121 +10,258 @@ 'LWRiZWMtNGI0YS05OWUxLTI3Y2FiNDdkOTI4Mg==') -class TestHotmart(unittest.TestCase): +class TestHelpers(unittest.TestCase): def setUp(self): self.hotmart = Hotmart(client_id=client_id, client_secret=client_secret, basic=basic) # Build Payload - def test_build_payload_with_all_values(self): - result = self.hotmart._build_payload(key1='value1', key2='value2') - self.assertEqual(result, {'key1': 'value1', 'key2': 'value2'}) - - def test_build_payload_with_none_values(self): - result = self.hotmart._build_payload(key1='value1', key2=None) - self.assertEqual(result, {'key1': 'value1'}) - - def test_build_payload_with_no_values(self): + def test_build_payload_with_valid_arguments(self): + """ + None values should be ignored + """ + result = self.hotmart._build_payload(buyer_email='test@example.com', + purchase_value=123, + is_buyer=True, + param4=None) + + self.assertEqual(result, { + "buyer_email": "test@example.com", + "purchase_value": 123, + "is_buyer": True + }) + + def test_build_payload_with_no_arguments(self): + """ + No arguments should return an empty dictionary + """ result = self.hotmart._build_payload() self.assertEqual(result, {}) - # Sandbox Mode - def test_sandbox_mode_true(self): - hotmart = Hotmart(client_id='123', client_secret='123', basic='123', - sandbox=True) - self.assertTrue(hotmart.sandbox) + # Handle Response + def test_handle_response_single_dict_response(self): + response = {"key": "value"} + expected_result = [response] + self.assertEqual(self.hotmart._handle_response(response), expected_result) - 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) + def test_handle_response_list_of_dicts_response(self): + response = [{"key1": "value1"}, {"key2": "value2"}] + self.assertEqual(self.hotmart._handle_response(response), response) - self.assertFalse(hotmart1.sandbox) - self.assertFalse(hotmart2.sandbox) + def test_handle_response_non_dict_non_list_response(self): + response = "invalid_response" + with self.assertRaises(ValueError): + self.hotmart._handle_response(response) # noqa - @patch('requests.get') - 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') - self.assertEqual(response, {"success": True}) + # Build URL + def test_build_url_with_valid_endpoint_payments(self): + result = self.hotmart._build_url('payments') + expected_result = 'https://developers.hotmart.com/payments/api/v1' + self.assertEqual(result, expected_result) - @patch('requests.get') - def test_http_error_request(self, mock_get): - mock_get.return_value.raise_for_status.side_effect = ( - requests.exceptions.HTTPError) + def test_build_url_with_valid_endpoint_club(self): + result = self.hotmart._build_url('club') + expected_result = 'https://developers.hotmart.com/club/api/v1' + self.assertEqual(result, expected_result) - with self.assertRaises(HTTPRequestException): - self.hotmart._make_request(requests.get, 'https://example.com') + def test_build_url_with_valid_endpoint_sandbox_payments(self): + self.hotmart.sandbox = True + result = self.hotmart._build_url('payments') + expected_result = 'https://sandbox.hotmart.com/payments/api/v1' + self.assertEqual(result, expected_result) - @patch('requests.get') - def test_request_exception(self, mock_get): - mock_get.side_effect = requests.exceptions.RequestException - with self.assertRaises(RequestException): - self.hotmart._make_request(requests.get, 'https://example.com') + def test_build_url_with_valid_endpoint_sandbox_club(self): + self.hotmart.sandbox = True + result = self.hotmart._build_url('club') + expected_result = 'https://sandbox.hotmart.com/club/api/v1' + self.assertEqual(result, expected_result) - @patch('requests.get') - def test_forbidden_request_in_sandbox_mode(self, mock_get): + def test_build_url_with_invalid_endpoint(self): + with self.assertRaises(ValueError): + self.hotmart._build_url('invalid_endpoint') + + # Sandbox Mode + def test_sandbox_mode_true(self): self.hotmart.sandbox = True - mock_get.return_value.status_code = 403 - mock_get.return_value.raise_for_status.side_effect = ( - requests.exceptions.HTTPError) + self.assertTrue(self.hotmart.sandbox) - with self.assertRaises(HTTPRequestException): - self.hotmart._make_request(requests.get, 'https://example.com') + def test_sandbox_mode_false(self): + self.assertFalse(self.hotmart.sandbox) + # Make Request @patch('requests.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) + def test_make_request_successful(self, mock_get): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.text = '{"items":[{"some":"info"}]}' + mock_response.json.return_value = { + "items": [{ + "some": "info" + }] + } + mock_get.return_value = mock_response + result = self.hotmart._make_request(requests.get, 'https://developers.hotmart.com/' + 'payments/api/v1/sales/history') + + self.assertEqual(result, { + "items": [{ + "some": "info" + }] + }) + + def test_make_request_http_error_403(self): + """ + Checks if the exception is raised and a log hint it's logged. + :return: + """ + method_mock = MagicMock() + url = 'https://developers.hotmart.com/payments/api/v1/sales/history' + headers = { + 'Authorization': basic, + 'Content-Type': 'application/json' + } + params = { + 'buyer_email': 'buyer@example.com' + } + body = { + 'subdomain': 'my_subdomain' + } + method_mock.side_effect = requests.exceptions.HTTPError(response=MagicMock(status_code=403)) + + with self.assertRaises(requests.exceptions.HTTPError) as e: + with patch.object(self.hotmart, 'logger') as mock_logger: + self.hotmart._make_request(method_mock, url, headers, params, body) + + self.assertEqual(e.exception.response.status_code, 403) + mock_logger.error.assert_called_with('Perhaps the credentials are for Sandbox Mode?') + + def test_make_request_http_error_422(self): + """ + Checks if the exception is raised and a log hint it's logged. + :return: + """ + method_mock = MagicMock() + url = 'https://developers.hotmart.com/payments/api/v1/sales/history' + headers = { + 'Authorization': basic, + 'Content-Type': 'application/json' + } + params = { + 'buyer_email': 'buyer@example.com' + } + body = { + 'subdomain': 'my_subdomain' + } + method_mock.side_effect = requests.exceptions.HTTPError(response=MagicMock(status_code=422)) - with self.assertRaises(HTTPRequestException): - self.hotmart._make_request(requests.get, 'https://example.com') + with self.assertRaises(requests.exceptions.HTTPError) as e: + with patch.object(self.hotmart, 'logger') as mock_logger: + self.hotmart._make_request(method_mock, url, headers, params, body) + self.assertEqual(e.exception.response.status_code, 422) + mock_logger.error.assert_called_with("This usually happens when the request is missing" + " body parameters.") + + def test_make_request_http_error_500(self): + """ + Checks if the exception is raised and a log hint it's logged. + :return: + """ + self.hotmart.sandbox = True + method_mock = MagicMock() + url = 'https://developers.hotmart.com/payments/api/v1/sales/history' + headers = { + 'Authorization': basic, + 'Content-Type': 'application/json' + } + params = { + 'buyer_email': 'buyer@example.com' + } + body = { + 'subdomain': 'my_subdomain' + } + method_mock.side_effect = requests.exceptions.HTTPError(response=MagicMock(status_code=500)) + + with self.assertRaises(requests.exceptions.HTTPError) as e: + with patch.object(self.hotmart, 'logger') as mock_logger: + self.hotmart._make_request(method_mock, url, headers, params, body) + + self.assertEqual(e.exception.response.status_code, 500) + mock_logger.error.assert_any_call('This happens with some endpoints in the Sandbox Mode.') + mock_logger.error.assert_any_call('Usually the API it\'s not down, it\'s just a bug.') + + def test_make_request_http_general_error(self): + """ + Checks if the exception is raised when errors are different from 401, 403, 422 or 500. + :return: + """ + method_mock = MagicMock() + url = 'https://developers.hotmart.com/payments/api/v1/sales/history' + headers = { + 'Authorization': basic, + 'Content-Type': 'application/json' + } + params = { + 'buyer_email': 'buyer@example.com' + } + body = { + 'subdomain': 'my_subdomain' + } + method_mock.side_effect = requests.exceptions.HTTPError(response=MagicMock(status_code=418)) + + with self.assertRaises(requests.exceptions.HTTPError) as e: + self.hotmart._make_request(method_mock, url, headers, params, body) + + self.assertEqual(e.exception.response.status_code, 418) + self.assertRaises(requests.exceptions.HTTPError) + + # Is token expired @patch('time.time') - def test_token_expired_when_expiry_in_past(self, mock_time): + def test_is_token_expired_when_expiry_in_past(self, mock_time): mock_time.return_value = 100 self.hotmart.token_expires_at = 99 self.assertTrue(self.hotmart._is_token_expired()) @patch('time.time') - def test_token_not_expired_when_expiry_in_future(self, mock_time): + def test_is_token_expired_when_expiry_in_future(self, mock_time): mock_time.return_value = 100 self.hotmart.token_expires_at = 101 self.assertFalse(self.hotmart._is_token_expired()) - @patch('requests.post') - def test_token_obtained_successfully(self, mock_post): - mock_post.return_value.json.return_value = { - 'access_token': 'test_token'} + # Fetch new token + @patch.object(Hotmart, '_make_request') + def test_fetch_new_token_obtained_successfully(self, mock_make_request): + mock_make_request.return_value = { + 'access_token': 'test_token' + } token = self.hotmart._fetch_new_token() self.assertEqual(token, 'test_token') @patch('requests.post') - def test_token_obtained_failure(self, mock_post): + def test_fetch_new_token_obtained_failure(self, mock_post): mock_post.side_effect = requests.exceptions.RequestException - with self.assertRaises(RequestException): + with self.assertRaises(requests.exceptions.RequestException): self.hotmart._fetch_new_token() + # Get token @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_get_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() + self.assertEqual(token, 'test_token') mock_fetch_new_token.assert_not_called() @patch.object(Hotmart, '_is_token_expired') @patch.object(Hotmart, '_fetch_new_token') - def test_token_not_in_cache_and_fetched_success(self, - mock_fetch_new_token, - mock_is_token_expired): + def test_get_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() @@ -131,93 +269,40 @@ def test_token_not_in_cache_and_fetched_success(self, @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_get_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() self.assertIsNone(token) + # Request with token @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_request_with_token_successful(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') - self.assertEqual(result, {"success": True}) + 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): - mock_get_token.return_value = 'test_token' - mock_make_request.side_effect = RequestException("Error", - "url") - with self.assertRaises(RequestException): - self.hotmart._request_with_token('GET', - 'https://example.com') + def test_request_with_token_failed(self, mock_make_request, mock_get_token): - @patch.object(Hotmart, '_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') + mock_make_request.side_effect = requests.exceptions.RequestException + with self.assertRaises(requests.exceptions.RequestException): + self.hotmart._request_with_token('GET', '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') - - 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) - - 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": {} - } - ] - params = {} - 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 + @patch.object(Hotmart, '_get_token') + def test_request_with_token_unsupported_method(self, mock_get_token): + mock_get_token.return_value = 'test_token' with self.assertRaises(ValueError): - self.hotmart._pagination('GET', - 'https://example.com', - paginate=True) + self.hotmart._request_with_token('PUT', 'https://example.com') if __name__ == '__main__': diff --git a/tests/test_sales.py b/tests/test_sales.py index 98c0340..1ec3243 100644 --- a/tests/test_sales.py +++ b/tests/test_sales.py @@ -1,224 +1,127 @@ import unittest from unittest.mock import patch +from requests import Response from hotmart_python import Hotmart client_id = 'b32450c1-1352-246a-b6d3-d49d6db815ea' client_secret = '90bcc221-cebd-5a5b-00e2-72cab47d9282' -basic = ('Basic YjIzNTQxYzAtMyEzNS20MjVhLWI1ZDItZDM4ZDVkYjcwNGVhOjA5Y2JiMTEzLWRiZWMtNGI0YS05OWUxLT\ -I3Y2FiNDdkOTI4Mg==') +basic = ('Basic YjIzNTQxYzAtMyEzNS20MjVhLWI1ZDItZDM4ZDVkYjcwNGVhOjA5Y2JiMTEz' + 'LWRiZWMtNGI0YS05OWUxLTI3Y2FiNDdkOTI4Mg==') -class TestHotmart(unittest.TestCase): +class TestSales(unittest.TestCase): def setUp(self): self.hotmart = Hotmart(client_id=client_id, client_secret=client_secret, basic=basic) - @patch.object(Hotmart, '_build_payload') - @patch.object(Hotmart, '_pagination') - def test_get_sales_history_with_pagination(self, mock_pagination, mock_build_payload): - 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_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") - self.assertEqual(result, [{"id": 1}, {"id": 2}]) - - @patch.object(Hotmart, '_build_payload') - @patch.object(Hotmart, '_pagination') - def test_get_sales_history_with_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_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" + @patch.object(Hotmart, '_request_with_token') + def test_get_sales_history(self, mock_req_with_token): + mock_response = Response() + mock_response.status_code = 200 + mock_response.return_value = { + 'items': [{ + 'some': 'info' + }] } - 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} - ]) - - @patch.object(Hotmart, '_build_payload') - @patch.object(Hotmart, '_pagination') - def test_get_sales_summary_no_pagination(self, mock_pagination, mock_build_payload): - mock_build_payload.return_value = { - "filter1": "value1" + mock_req_with_token.return_value = mock_response + + self.hotmart.get_sales_history(buyer_name='Paula', payment_type='BILLET') + expected_url = 'https://developers.hotmart.com/payments/api/v1/sales/history' + + mock_req_with_token.assert_called_once_with(method="get", + url=expected_url, + params={ + 'buyer_name': 'Paula', + 'payment_type': 'BILLET' + }, + enhance=True) + + @patch.object(Hotmart, '_request_with_token') + def test_get_sales_summary(self, mock_req_with_token): + mock_response = Response() + mock_response.status_code = 200 + mock_response.return_value = { + 'items': [{ + 'some': 'info' + }] } - 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} - ]) - - @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_req_with_token.return_value = mock_response + + self.hotmart.get_sales_summary(buyer_name='Paula', payment_type='BILLET') + expected_url = 'https://developers.hotmart.com/payments/api/v1/sales/summary' + + mock_req_with_token.assert_called_once_with(method="get", + url=expected_url, + params={ + 'buyer_name': 'Paula', + 'payment_type': 'BILLET' + }, + enhance=True) + + @patch.object(Hotmart, '_request_with_token') + def test_get_sales_participants(self, mock_req_with_token): + mock_response = Response() + mock_response.status_code = 200 + mock_response.return_value = { + 'items': [{ + 'some': 'info' + }] } - 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} - ] - result = self.hotmart.get_sales_participants(paginate=True, filter1="value1") - - self.assertEqual(result, [ - {"id": 1}, - {"id": 2} - ]) - - @patch.object(Hotmart, '_build_payload') - @patch.object(Hotmart, '_pagination') - 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} - ]) - - @patch.object(Hotmart, '_build_payload') - @patch.object(Hotmart, '_pagination') - 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_req_with_token.return_value = mock_response + + self.hotmart.get_sales_participants(buyer_name='Paula', transaction_status='APPROVED') + expected_url = 'https://developers.hotmart.com/payments/api/v1/sales/users' + + mock_req_with_token.assert_called_once_with(method="get", + url=expected_url, + params={ + 'buyer_name': 'Paula', + 'transaction_status': 'APPROVED' + }, + enhance=True) + + @patch.object(Hotmart, '_request_with_token') + def test_get_sales_commissions(self, mock_req_with_token): + mock_response = Response() + mock_response.status_code = 200 + mock_response.return_value = { + 'items': [{ + 'some': 'info' + }] } - 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} - ]) - - @patch.object(Hotmart, '_build_payload') - @patch.object(Hotmart, '_pagination') - def test_sales_commissions_retrieval_no_pagination(self, mock_pagination, mock_build_payload): - mock_build_payload.return_value = { - "filter1": "value1" + mock_req_with_token.return_value = mock_response + + self.hotmart.get_sales_commissions(commission_as='PRODUCER', transaction_status='APPROVED') + expected_url = 'https://developers.hotmart.com/payments/api/v1/sales/commissions' + + mock_req_with_token.assert_called_once_with(method="get", + url=expected_url, + params={ + 'commission_as': 'PRODUCER', + 'transaction_status': 'APPROVED' + }, + enhance=True) + + @patch.object(Hotmart, '_request_with_token') + def test_get_sales_price_details(self, mock_req_with_token): + mock_response = Response() + mock_response.status_code = 200 + mock_response.return_value = { + 'items': [{ + 'some': 'info' + }] } - 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} - ]) - - @patch.object(Hotmart, '_build_payload') - @patch.object(Hotmart, '_pagination') - 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} - ] - result = self.hotmart.get_sales_price_details(paginate=True, filter1="value1") - self.assertEqual(result, [ - {"id": 1}, - {"id": 2} - ]) - - @patch.object(Hotmart, '_build_payload') - @patch.object(Hotmart, '_pagination') - 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} - ]) - - @patch.object(Hotmart, '_build_payload') - @patch.object(Hotmart, '_pagination') - 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) + mock_req_with_token.return_value = mock_response + + self.hotmart.get_sales_price_details(payment_type='CREDIT_CARD', + transaction_status='APPROVED') + expected_url = 'https://developers.hotmart.com/payments/api/v1/sales/price/details' + + mock_req_with_token.assert_called_once_with(method="get", + url=expected_url, + params={ + 'payment_type': 'CREDIT_CARD', + 'transaction_status': 'APPROVED' + }, + enhance=True) diff --git a/tests/test_subscriptions.py b/tests/test_subscriptions.py index 61cd6e1..39b8fcf 100644 --- a/tests/test_subscriptions.py +++ b/tests/test_subscriptions.py @@ -1,6 +1,7 @@ import unittest from unittest.mock import patch -from hotmart_python import Hotmart, RequestException +from requests import Response +from hotmart_python import Hotmart client_id = 'b32450c1-1352-246a-b6d3-d49d6db815ea' client_secret = '90bcc221-cebd-5a5b-00e2-72cab47d9282' @@ -8,85 +9,137 @@ 'LWRiZWMtNGI0YS05OWUxLTI3Y2FiNDdkOTI4Mg==') -class TestHotmart(unittest.TestCase): +class TestSubscriptions(unittest.TestCase): def setUp(self): 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): - mock_request_with_token.return_value = {"subscriptions": [{"id": 1}, {"id": 2}]} - result = self.hotmart.get_subscriptions(paginate=False) - self.assertEqual(result, {"subscriptions": [{"id": 1}, {"id": 2}]}) + def test_get_subscriptions_success(self, mock_req_with_token): + mock_response = Response() + mock_response.status_code = 200 + mock_response.return_value = { + 'items': [{ + 'some': 'info' + }] + } + mock_req_with_token.return_value = mock_response - @patch.object(Hotmart, '_request_with_token') - def should_raise_exception_when_retrieving_subscriptions_fails(self, mock_request_with_token): - mock_request_with_token.side_effect = RequestException("Error", "url") - with self.assertRaises(RequestException): - self.hotmart.get_subscriptions(paginate=False) + self.hotmart.get_subscriptions(param1='value1', param2='value2') + expected_url = 'https://developers.hotmart.com/payments/api/v1/subscriptions' - @patch.object(Hotmart, '_request_with_token') - def should_retrieve_subscription_summary_when_valid_request(self, mock_request_with_token): - mock_request_with_token.return_value = {"summary": {"total": 10}} - result = self.hotmart.get_subscriptions_summary(paginate=False) - self.assertEqual(result, {"summary": {"total": 10}}) + mock_req_with_token.assert_called_once_with(method="get", url=expected_url, + params={'param1': 'value1', 'param2': 'value2'}, + enhance=True) @patch.object(Hotmart, '_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) + def test_get_subscriptions_summary_success(self, mock_req_with_token): + mock_response = Response() + mock_response.status_code = 200 + mock_response.return_value = { + 'items': [{ + 'some': 'info' + }] + } + mock_req_with_token.return_value = mock_response - @patch.object(Hotmart, '_request_with_token') - def should_retrieve_subscription_purchases_when_valid_request(self, mock_request_with_token): - mock_request_with_token.return_value = {"purchases": [{"id": 1}, {"id": 2}]} - result = self.hotmart.get_subscription_purchases(subscriber_code="123", paginate=False) - self.assertEqual(result, {"purchases": [{"id": 1}, {"id": 2}]}) + self.hotmart.get_subscriptions_summary(param1='value1', param2='value2') + expected_url = 'https://developers.hotmart.com/payments/api/v1/subscriptions/summary' - @patch.object(Hotmart, '_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) + mock_req_with_token.assert_called_once_with(method="get", url=expected_url, + params={'param1': 'value1', 'param2': 'value2'}, + enhance=True) @patch.object(Hotmart, '_request_with_token') - def should_cancel_subscription_when_valid_request(self, mock_request_with_token): - mock_request_with_token.return_value = {"status": "cancelled"} - result = self.hotmart.cancel_subscription(subscriber_code=["123"], send_email=True) - self.assertEqual(result, {"status": "cancelled"}) + def test_get_subscriptions_purchases_success(self, mock_req_with_token): + mock_response = Response() + mock_response.status_code = 200 + mock_response.return_value = { + 'items': [{ + 'some': 'info' + }] + } + mock_req_with_token.return_value = mock_response - @patch.object(Hotmart, '_request_with_token') - def should_raise_exception_when_cancelling_subscription_fails(self, mock_request_with_token): - mock_request_with_token.side_effect = RequestException("Error", "url") - with self.assertRaises(RequestException): - self.hotmart.cancel_subscription(subscriber_code=["123"], send_email=True) + subscriber_code = "HTMT20219" - @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) - self.assertEqual(result, {"status": "reactivated"}) + self.hotmart.get_subscription_purchases(subscriber_code=subscriber_code, param1='value1', + param2='value2') + expected_url = (f'https://developers.hotmart.com/payments/api/v1' + f'/subscriptions/{subscriber_code}/purchases') + + mock_req_with_token.assert_called_once_with(method="get", url=expected_url, + params={'param1': 'value1', 'param2': 'value2'}, + enhance=True) @patch.object(Hotmart, '_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) + def test_cancel_subscriptions(self, mock_req_with_token): + mock_response = Response() + mock_response.status_code = 200 + mock_response.return_value = { + 'items': [{ + 'some': 'info' + }] + } + mock_req_with_token.return_value = mock_response + + subscriber_code = ["HTMT20219"] + expected_url = ('https://developers.hotmart.com/payments/api/v1' + '/subscriptions/cancel') + expected_body = { + 'subscriber_code': subscriber_code, + 'send_email': True + } + + self.hotmart.cancel_subscription(subscriber_code=subscriber_code) + + mock_req_with_token.assert_called_once_with(method="post", url=expected_url, + body=expected_body) @patch.object(Hotmart, '_request_with_token') - def should_change_due_day_when_valid_request(self, mock_request_with_token): - mock_request_with_token.return_value = {"status": "due day changed"} - result = self.hotmart.change_due_day(subscriber_code="123", new_due_day=15) - self.assertEqual(result, {"status": "due day changed"}) + def test_reactivate_and_charge_subscription(self, mock_req_with_token): + mock_response = Response() + mock_response.status_code = 200 + mock_response.return_value = { + 'items': [{ + 'some': 'info' + }] + } + mock_req_with_token.return_value = mock_response + + subscriber_code = ["HTMT20219"] + expected_url = ('https://developers.hotmart.com/payments/api/v1' + '/subscriptions/reactivate') + expected_body = { + 'subscriber_code': subscriber_code, + 'charge': False + } + + self.hotmart.reactivate_and_charge_subscription(subscriber_code=subscriber_code) + + mock_req_with_token.assert_called_once_with(method="post", url=expected_url, + body=expected_body) @patch.object(Hotmart, '_request_with_token') - def should_raise_exception_when_changing_due_day_fails(self, mock_request_with_token): - mock_request_with_token.side_effect = RequestException("Error", "url") - with self.assertRaises(RequestException): - self.hotmart.change_due_day(subscriber_code="123", new_due_day=15) + def test_change_due_day(self, mock_req_with_token): + mock_response = Response() + mock_response.status_code = 200 + mock_response.return_value = { + 'items': [{ + 'some': 'info' + }] + } + mock_req_with_token.return_value = mock_response + + subscriber_code = "HTMT20219" + expected_url = (f'https://developers.hotmart.com/payments/api/v1' + f'/subscriptions/{subscriber_code}') + expected_body = { + 'due_day': 25 + } + + self.hotmart.change_due_day(subscriber_code, 25) + + mock_req_with_token.assert_called_once_with(method="patch", url=expected_url, + body=expected_body)