diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd832cd5..eb02128a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/pypi-push.yml b/.github/workflows/pypi-push.yml index 375580c1..41d67b4f 100644 --- a/.github/workflows/pypi-push.yml +++ b/.github/workflows/pypi-push.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index d14e7967..b26235bd 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,42 @@ python3 -m unittest tests The Lago documentation is available at [doc.getlago.com](https://doc.getlago.com/docs/api/intro). +## Changelog + +* [#55](https://github.com/getlago/lago-python-client/pull/55) -- Error handling (`LagoApiError`) + + +Example, creating wallet: + +``` +try: + response = client.wallets().create(wallet) +except LagoApiError as error: + do_something(status=error.status_code) +``` +### Available properties: +``` +>>> error.status_code +422 + +>>> error.detail +'Unprocessable Entity' + +>>> error.headers +{'X-Frame-Options': 'SAMEORIGIN', 'X-XSS-Protection': '0', 'X-Content-Type-Options': 'nosniff', 'X-Download-Options': 'noopen', 'X-Permitted-Cross-Domain-Policies': 'none', 'Referrer-Policy': 'strict-origin-when-cross-origin', 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-cache', 'X-Request-Id': '613e7542-b29e-4224-bd19-a16dd1bfa62b', 'X-Runtime': '1.024304', 'Vary': 'Origin', 'Transfer-Encoding': 'chunked'} + +>>> error.response +{'status': 422, 'error': 'Unprocessable Entity', 'code': 'validation_errors', 'error_details': {'customer': ['wallet_already_exists']}} + +>>> error.response['error_details']['customer'][0] +'wallet_already_exists' + + +>>> error.url +'http://localhost:3000/api/v1/wallets' +``` + + ## Contributing The contribution documentation is available [here](https://github.com/getlago/lago-python-client/blob/main/CONTRIBUTING.md) diff --git a/lago_python_client/clients/base_client.py b/lago_python_client/clients/base_client.py index 23e180bb..a9a41ecb 100644 --- a/lago_python_client/clients/base_client.py +++ b/lago_python_client/clients/base_client.py @@ -1,10 +1,14 @@ -import requests +from collections.abc import MutableMapping import json +from http import HTTPStatus +from typing import Any +from urllib.parse import urljoin, urlencode +import orjson from pydantic import BaseModel +import requests from requests import Response -from typing import Dict -from urllib.parse import urljoin, urlencode + from lago_python_client.version import LAGO_VERSION @@ -15,7 +19,7 @@ def __init__(self, base_url: str, api_key: str): self.base_url = base_url self.api_key = api_key - def find(self, resource_id: str, params: Dict = None): + def find(self, resource_id: str, params: dict | None = None): api_resource = self.api_resource() + '/' + resource_id query_url = urljoin(self.base_url, api_resource) @@ -28,7 +32,7 @@ def find(self, resource_id: str, params: Dict = None): return self.prepare_response(data) - def find_all(self, options: Dict = None): + def find_all(self, options: dict | None = None): if options: api_resource = self.api_resource() + '?' + urlencode(options) else: @@ -64,7 +68,7 @@ def create(self, input_object: BaseModel): else: return self.prepare_response(data.json().get(self.root_name())) - def update(self, input_object: BaseModel, identifier: str = None): + def update(self, input_object: BaseModel, identifier: str | None = None): api_resource = self.api_resource() if identifier is not None: @@ -91,19 +95,28 @@ def headers(self): return headers - def handle_response(self, response: Response): + def handle_response(self, response: Response) -> Response | None: if response.status_code in BaseClient.RESPONSE_SUCCESS_CODES: if response.text: return response else: return None else: + if response.text: + response_data: Any = orjson.loads(response.text) + detail: str | None = getattr(response_data, 'error', None) + else: + response_data = None + detail = None raise LagoApiError( - "URI: %s. Status code: %s. Response: %s." % ( - response.request.url, response.status_code, response.text) + status_code=response.status_code, + url=response.request.url, + response=response_data, + detail=detail, + headers=response.headers, ) - def prepare_index_response(self, data: Dict): + def prepare_index_response(self, data: dict): collection = [] for el in data[self.api_resource()]: @@ -118,4 +131,22 @@ def prepare_index_response(self, data: Dict): class LagoApiError(Exception): - ... + def __init__( + self, + status_code: int, + url: str | None, + response: Any, + detail: str | None = None, + headers: MutableMapping[str, str] | None = None, + ) -> None: + if detail is None: + detail = HTTPStatus(status_code).phrase + self.status_code = status_code + self.url = url + self.response = response + self.detail = detail + self.headers = headers + + def __repr__(self) -> str: + class_name = self.__class__.__name__ + return f"{class_name}(status_code={self.status_code!r}, detail={self.detail!r})" diff --git a/setup.cfg b/setup.cfg index 4cb20b6b..816aa3f1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,6 +19,7 @@ package_dir = packages = find: python_requires = >3.6 install_requires = + orjson requests==2.26.0 pydantic requests-mock