Skip to content

Commit

Permalink
Update Liqpay types, exceptions, server,
Browse files Browse the repository at this point in the history
validation, and convert modules
  • Loading branch information
rostyq committed Nov 9, 2023
1 parent c17db6d commit cb1543d
Show file tree
Hide file tree
Showing 10 changed files with 367 additions and 147 deletions.
126 changes: 48 additions & 78 deletions liqpy/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@

from functools import singledispatchmethod
from enum import Enum
from dataclasses import asdict

from urllib.parse import urljoin
from base64 import b64encode, b64decode
from hashlib import sha1
from json import dumps, loads, JSONEncoder
from json import loads, JSONEncoder

from uuid import UUID
from decimal import Decimal
from datetime import date, datetime, UTC

from .data import FiscalItem, DetailAddenda, SplitRule
from .convert import to_datetime, to_milliseconds
from .preprocess import Preprocessor, BasePreprocessor
from .validation import Validator, BaseValidator

if TYPE_CHECKING:
Expand Down Expand Up @@ -59,6 +60,18 @@ def url(self) -> str:
class LiqPayJSONEncoder(JSONEncoder):
date_fmt = r"%Y-%m-%d %H:%M:%S"

def __init__(self) -> None:
super().__init__(
skipkeys=False,
ensure_ascii=True,
check_circular=True,
allow_nan=False,
sort_keys=False,
indent=None,
separators=None,
default=None,
)

@singledispatchmethod
def default(self, o):
return super().default(o)
Expand All @@ -85,35 +98,15 @@ def _(self, o: UUID) -> str:

@default.register
def _(self, o: DetailAddenda) -> str:
return encode(
{
"airLine": o.air_line,
"ticketNumber": o.ticket_number,
"passengerName": o.passenger_name,
"flightNumber": o.flight_number,
"originCity": o.origin_city,
"destinationCity": o.destination_city,
"departureDate": o.departure_date.strftime(r"%d%m%y"),
}
)
return b64encode(self.encode(o.to_dict()).encode()).decode()

@default.register
def _(self, o: SplitRule) -> dict:
return {
"public_key": o.public_key,
"amount": o.amount,
"commission_payer": o.commission_payer,
"server_url": o.server_url,
}
return asdict(o)

@default.register
def _(self, o: FiscalItem) -> dict:
return {
"id": o.id,
"amount": o.amount,
"cost": o.cost,
"price": o.price,
}
return asdict(o)


def is_sandbox(key: str, /) -> bool:
Expand Down Expand Up @@ -204,40 +197,37 @@ def sign(data: bytes, /, key: bytes) -> bytes:


def encode(
params: "LiqpayRequestDict", /, *, validator: type[BaseValidator] = Validator
params: "LiqpayRequestDict",
/,
*,
filter_none: bool = True,
validator: Optional[BaseValidator] = None,
encoder: Optional[JSONEncoder] = None,
preprocessor: Optional[BasePreprocessor] = None,
) -> bytes:
"""
Encode parameters into base64 encoded JSON.
>>> encode({"action": "status", "version": 3})
b'eyJhY3Rpb24iOiAic3RhdHVzIiwgInZlcnNpb24iOiAzfQ=='
"""
validator()(params)

dae = params.get("dae")
if isinstance(dae, dict):
params["dae"] = DetailAddenda(**dae)

split_rules = params.get("split_rules")
if split_rules is not None and isinstance(split_rules, list):
params["split_rules"] = dumps(split_rules, cls=LiqPayJSONEncoder)

paytypes = params.get("paytypes")
if paytypes is not None and isinstance(paytypes, list):
params["paytypes"] = ",".join(paytypes)

s = dumps(
obj=params,
skipkeys=False,
ensure_ascii=True,
check_circular=True,
indent=None,
allow_nan=False,
separators=None,
sort_keys=False,
cls=LiqPayJSONEncoder,
)
return b64encode(s.encode())
if filter_none:
params = {key: value for key, value in params.items() if value is not None}

if encoder is None:
encoder = LiqPayJSONEncoder()

if preprocessor is None:
preprocessor = Preprocessor()

preprocessor(params, encoder=encoder)

if validator is None:
validator = Validator()

validator(params)

return b64encode(encoder.encode(params).encode())


def decode(data: bytes, /) -> dict[str, Any]:
Expand All @@ -259,43 +249,23 @@ def request(
>>> request("status", key="...", order_id="a1a1a1a1")
{'action': 'status', 'public_key': '...', 'version': 3, 'order_id': 'a1a1a1a1'}
"""
params = {k: v for k, v in params.items() if v is not None}
params.update(action=action, public_key=public_key, version=version)

match action:
case "reports":
params.update(
date_from=to_milliseconds(params["date_from"]),
date_to=to_milliseconds(params["date_to"]),
)
return params

case "status" | "invoice_cancel" | "unsubscribe" | "refund" | "data":
return params

case "auth":
if params.get("verifycode", False):
params["verifycode"] = "Y"

case "subscribe":
subscribe_date_start = params.get("subscribe_date_start")

if subscribe_date_start is None:
subscribe_date_start = datetime.now(UTC)

assert "subscribe_periodicity" in params, "subscribe_periodicity is required"

params.update(
subscribe=1,
subscribe_date_start=to_datetime(subscribe_date_start),
subscribe=True,
subscribe_date_start=subscribe_date_start,
)

case "letter_of_credit":
params.update(
letter_of_credit=1,
letter_of_credit_date=to_datetime(params.get("letter_of_credit_date")),
)

if params.get("recurringbytoken", False):
assert "server_url" in params, "server_url must be specified"
params["reccuringbytoken"] = "1"
params["letter_of_credit"] = True

return params
79 changes: 61 additions & 18 deletions liqpy/client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Optional, Literal, Union, TYPE_CHECKING, Unpack
from typing import Optional, Literal, Union, TYPE_CHECKING, Unpack, AnyStr
from os import environ
from logging import getLogger
from datetime import datetime
from datetime import datetime, timedelta
from numbers import Number
from re import search
from uuid import UUID
Expand All @@ -13,11 +13,17 @@

from .api import post, Endpoint, sign, request, encode, decode, VERSION, is_sandbox
from .exceptions import exception_factory
from .data import LiqpayCallback

if TYPE_CHECKING:
from .types.common import Language, Currency, SubscribePeriodicity
from json import JSONEncoder

from .preprocess import BasePreprocessor
from .validation import BaseValidator

from .types.common import Language, Currency, SubscribePeriodicity, PayOption
from .types.request import Format, Language, LiqpayRequestDict
from .types.callback import CallbackDict
from .types.callback import LiqpayCallbackDict


__all__ = ["Client"]
Expand Down Expand Up @@ -61,17 +67,28 @@ class Client:
_public_key: str
_private_key: Secret[bytes]

validator: Optional["BaseValidator"] = None
preprocessor: Optional["BasePreprocessor"] = None
encoder: Optional["JSONEncoder"] = None

def __init__(
self,
/,
public_key: str | None = None,
private_key: str | None = None,
*,
session: Session = None,
validator: Optional["BaseValidator"] = None,
preprocessor: Optional["BasePreprocessor"] = None,
encoder: Optional["JSONEncoder"] = None,
):
self.update_keys(public_key=public_key, private_key=private_key)
self.session = session

self.validator = validator
self.preprocessor = preprocessor
self.encoder = encoder

@property
def public_key(self) -> str:
"""Public key used for requests."""
Expand Down Expand Up @@ -132,7 +149,7 @@ def __del__(self):

def _callback(
self, /, data: bytes, signature: bytes, *, verify: bool = True
) -> "CallbackDict":
) -> "LiqpayCallbackDict":
if verify:
self.verify(data, signature)
else:
Expand Down Expand Up @@ -160,7 +177,11 @@ def encode(
See `liqpy.api.encode` for more information.
"""
data = encode(
request(action, public_key=self._public_key, version=VERSION, **kwargs)
request(action, public_key=self._public_key, version=VERSION, **kwargs),
filter_none=True,
validator=self.validator,
preprocessor=self.preprocessor,
encoder=self.encoder,
)
signature = self.sign(data)

Expand Down Expand Up @@ -196,20 +217,23 @@ def request(self, action: str, **kwargs: "LiqpayRequestDict") -> dict:
raise exception_factory(response=response)

data: dict = response.json()

result: Optional[Literal["ok", "error"]] = data.pop("result", None)
status = data.get("status")
err_code = data.pop("err_code", data.pop("code", None))

if result == "ok" or action in ("status", "data"):
if result == "ok" or (action in ("status", "data") and err_code is None):
return data
elif status in ("error", "failure"):

if status in ("error", "failure") or result == "error":
raise exception_factory(
code=data.pop("err_code", None),
code=err_code,
description=data.pop("err_description", None),
response=response,
details=data,
)
else:
return data

return data

def pay(
self,
Expand Down Expand Up @@ -245,6 +269,8 @@ def checkout(
amount: Number,
currency: "Currency",
description: str,
expired_date: str | datetime | None = None,
paytypes: Optional[list["PayOption"]] = None,
**kwargs: Unpack["LiqpayRequestDict"],
) -> str:
"""
Expand All @@ -257,13 +283,19 @@ def checkout(
assert (
action in CHECKOUT_ACTIONS
), "Invalid action. Must be one of: %s" % ",".join(CHECKOUT_ACTIONS)
kwargs.update(
order_id=order_id, amount=amount, currency=currency, description=description
)

response = post(
Endpoint.CHECKOUT,
*self.encode(action, **kwargs),
*self.encode(
action,
order_id=order_id,
amount=amount,
currency=currency,
description=description,
expired_date=expired_date,
paytypes=paytypes,
**kwargs,
),
session=self._session,
allow_redirects=False,
)
Expand Down Expand Up @@ -354,6 +386,7 @@ def subscribe(
currency: "Currency",
description: str,
subscribe_periodicity: "SubscribePeriodicity",
subscribe_date_start: datetime | str | timedelta | None | Number,
**kwargs: Unpack["LiqpayRequestDict"],
) -> dict:
return self.request(
Expand All @@ -366,6 +399,7 @@ def subscribe(
card_exp_year=card_exp_year,
currency=currency,
description=description,
subscribe_date_start=subscribe_date_start,
subscribe_periodicity=subscribe_periodicity,
**kwargs,
)
Expand Down Expand Up @@ -408,7 +442,7 @@ def status(self, order_id: str | UUID, /) -> dict:
"""
return self.request("status", order_id=order_id)

def callback(self, /, data: str, signature: str, *, verify: bool = True):
def callback(self, /, data: AnyStr, signature: AnyStr, *, verify: bool = True):
"""
Verify and decode the callback data.
Expand All @@ -433,10 +467,19 @@ def callback(self, /, data: str, signature: str, *, verify: bool = True):
[Documentation](https://www.liqpay.ua/en/documentation/api/callback)
"""
result = self._callback(data.encode(), signature.encode(), verify=verify)
if isinstance(data, str):
data = data.encode()

if isinstance(signature, str):
signature = signature.encode()

result = self._callback(data, signature, verify=verify)
version = result.get("version")

if version != VERSION:
logger.warning("Callback version mismatch: %s != %s", version, VERSION)

return result
try:
return LiqpayCallback(**result)
finally:
logger.warning("Failed to parse callback data.", extra=result)
Loading

0 comments on commit cb1543d

Please sign in to comment.