Skip to content
This repository has been archived by the owner on Jul 16, 2024. It is now read-only.

Commit

Permalink
decouple http package from API
Browse files Browse the repository at this point in the history
  • Loading branch information
litteratum committed Feb 4, 2024
1 parent 739d6c3 commit 991d06a
Show file tree
Hide file tree
Showing 25 changed files with 1,125 additions and 941 deletions.
3 changes: 2 additions & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ disable=raw-checker-failed,
useless-suppression,
deprecated-pragma,
use-symbolic-message-instead,
fixme
fixme,
duplicate-code
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* Fix CI. Use Python 3.9, 3.11

### Changed

* `http` package decoupled from the API


## [0.4.1] - 2023-11-14
### Added
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ response = client.refund_payment(pay_id, amount=100)

### Exceptions handling
```python
from csobclient.v19 import APIError, HTTPRequestError
from csobclient.v19 import APIError
from csobclient.http import HTTPRequestError

try:
response = client.operation(...)
Expand Down
16 changes: 16 additions & 0 deletions csobclient/http/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Package for dealing with HTTP."""
from .base import (
HTTPClient,
HTTPConnectionError,
HTTPRequestError,
HTTPResponse,
HTTPTimeoutError,
)

__all__ = [
"HTTPClient",
"HTTPConnectionError",
"HTTPRequestError",
"HTTPResponse",
"HTTPTimeoutError",
]
117 changes: 117 additions & 0 deletions csobclient/http/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"""Base client."""

import json as jsonlib
from abc import ABC, abstractmethod
from typing import Optional

_DEFAULT_REQUEST_TIMEOUT = 5


class HTTPRequestError(Exception):
"""Base HTTP request error."""


class HTTPConnectionError(HTTPRequestError):
"""Any error related to connection."""


class HTTPTimeoutError(HTTPRequestError):
"""HTTP request timed out."""


class HTTPInvalidResponseError(HTTPRequestError):
"""HTTP response is invalid."""


class HTTPResponse:
"""HTTP response wrapper."""

# pylint: disable=too-few-public-methods
def __init__(
self, status_code: int, body: Optional[bytes], headers: dict
) -> None:
self.status_code = status_code
self.body = body or b""
self._headers = headers

self._json = None

@property
def json(self) -> Optional[dict]:
"""Return body as JSON."""
if self._json is not None:
return self._json

headers = {key.lower(): val for key, val in self._headers.items()}
if "application/json" in headers.get("content-type", ""):
try:
self._json = jsonlib.loads(self.body)
except Exception as exc:
raise HTTPInvalidResponseError(
f"Invalid JSON in response: {exc}"
) from exc

return self._json

@property
def success(self) -> bool:
"""Return whether HTTP request was successful."""
return self.status_code < 400

def __str__(self) -> str:
return f"{self.__class__.__name__}(status={self.status_code})"

def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"status={self.status_code}, "
f"body={self.body}, "
f"headers={self._headers}"
")"
)


class HTTPClient(ABC):
# pylint:disable=too-few-public-methods
"""Base HTTP client."""

def __init__(self, timeout: int = _DEFAULT_REQUEST_TIMEOUT) -> None:
self.timeout = timeout

# pylint: disable=too-many-arguments
@abstractmethod
def _request(
self,
method: str,
url: str,
json: Optional[dict] = None,
headers: Optional[dict] = None,
) -> HTTPResponse:
"""Perform request.
This method must handle all possible HTTP exceptions and raise them
as `HTTPRequestError`.
Always use `headers`. You may extend it when needed.
:param url: API method URL
:param method: HTTP method
:param json: JSON data to post
:param headers: headers to be sent
"""

def request(
self,
method: str,
url: str,
json: Optional[dict] = None,
headers: Optional[dict] = None,
) -> HTTPResponse:
"""Perform HTTP request with a given HTTP method.
:param method: HTTP method to use
:param url: API URL
:param json: JSON data to post
:param headers: headers
"""
return self._request(method, url, json, headers=headers)
54 changes: 54 additions & 0 deletions csobclient/http/requests_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""HTTP client which uses `requests` under the hood."""

from typing import Optional

import requests

from .base import (
_DEFAULT_REQUEST_TIMEOUT,
HTTPClient,
HTTPConnectionError,
HTTPRequestError,
HTTPResponse,
HTTPTimeoutError,
)


class RequestsHTTPClient(HTTPClient):
# pylint: disable=too-few-public-methods
"""`requests` HTTP client."""

def __init__(self, timeout: int = _DEFAULT_REQUEST_TIMEOUT) -> None:
super().__init__(timeout)
self._session = requests.Session()

# pylint: disable=too-many-arguments
def _request(
self,
method: str,
url: str,
json: Optional[dict] = None,
headers: Optional[dict] = None,
) -> HTTPResponse:
try:
response: requests.Response = getattr(
self._session, method.lower()
)(
url,
json=json,
timeout=self.timeout,
headers=headers,
)
except ConnectionError as exc:
raise HTTPConnectionError(exc) from exc
except requests.Timeout as exc:
raise HTTPTimeoutError(exc) from exc
except requests.RequestException as exc:
raise HTTPRequestError(exc) from exc

return HTTPResponse(
response.status_code, response.content, dict(response.headers)
)

def __str__(self) -> str:
return self.__class__.__name__
15 changes: 4 additions & 11 deletions csobclient/v19/__init__.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
"""Client for API v.1.9."""
from .client import Client

from .cart import Cart, CartItem
from .client import Client
from .currency import Currency
from .key import CachedRSAKey, FileRSAKey, RSAKey
from .payment import (
APIError,
PaymentInfo,
PaymentMethod,
PaymentOperation,
PaymentStatus,
)
from .webpage import WebPageAppearanceConfig, WebPageLanguage
from .key import RSAKey, FileRSAKey, CachedRSAKey
from .signature import InvalidSignatureError
from .http import (
HTTPClient,
HTTPConnectionError,
HTTPRequestError,
HTTPResponse,
HTTPTimeoutError,
RequestsHTTPClient,
)
from .webpage import WebPageAppearanceConfig, WebPageLanguage
29 changes: 15 additions & 14 deletions csobclient/v19/client.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
"""Client."""

from enum import Enum
from typing import Optional, Union

from .currency import Currency
from .payment import PaymentMethod, PaymentOperation, PaymentInfo
from ..http import HTTPClient, HTTPResponse
from ..http.requests_client import RequestsHTTPClient
from .cart import Cart, CartItem
from .currency import Currency
from .dttm import get_dttm, get_payment_expiry
from .key import CachedRSAKey, FileRSAKey, RSAKey
from .merchant import _MerchantData
from .payment import PaymentInfo, PaymentMethod, PaymentOperation
from .signature import mk_payload, mk_url, verify
from .webpage import WebPageAppearanceConfig
from .dttm import get_dttm, get_payment_expiry
from .signature import mk_payload, verify, mk_url
from .http import RequestsHTTPClient, HTTPClient, HTTPResponse
from .key import FileRSAKey, CachedRSAKey, RSAKey

# from .customer import CustomerData
# from .order import OrderData
Expand Down Expand Up @@ -115,7 +117,7 @@ def init_payment(
),
)
response = self._http_client.request(
f"{self.base_url}/payment/init", json=payload
"post", f"{self.base_url}/payment/init", json=payload
)
return self._get_payment_info(response)

Expand Down Expand Up @@ -164,10 +166,11 @@ def get_payment_status(self, pay_id: str):
f"{self.base_url}/payment/status/",
payload=self._build_payload(pay_id=pay_id),
)
response = self._http_client.request(url, method="get")
response = self._http_client.request("get", url)
return self._get_payment_info(response)

def process_gateway_return(self, datadict: dict) -> PaymentInfo:
# pylint:disable=no-self-use
"""Process gateway return."""
data = {}

Expand All @@ -178,9 +181,7 @@ def process_gateway_return(self, datadict: dict) -> PaymentInfo:
else datadict[key]
)

return self._get_payment_info(
HTTPResponse(http_success=True, data=data)
)
return PaymentInfo.from_response(data)

def reverse_payment(self, pay_id: str) -> PaymentInfo:
"""Reverse payment.
Expand Down Expand Up @@ -211,7 +212,7 @@ def refund_payment(
return self._get_payment_info(response)

def _get_payment_info(self, response: HTTPResponse) -> PaymentInfo:
if response.http_success:
verify(response.data, str(self.public_key))
if response.success:
verify(response.json, str(self.public_key))

return PaymentInfo.from_response(response.data)
return PaymentInfo.from_response(response.json)
9 changes: 0 additions & 9 deletions csobclient/v19/http/__init__.py

This file was deleted.

44 changes: 0 additions & 44 deletions csobclient/v19/http/client.py

This file was deleted.

Loading

0 comments on commit 991d06a

Please sign in to comment.